Input component with storybook and testing

This commit is contained in:
adilallo
2025-10-10 09:07:47 -06:00
parent 04783d3f62
commit 2bc5fcdf45
12 changed files with 1838 additions and 46 deletions
+2 -3
View File
@@ -1,6 +1,6 @@
"use client";
import React, { memo } from "react";
import React, { memo, useId } from "react";
/**
* Checkbox
@@ -83,8 +83,7 @@ const Checkbox = memo(
};
// Generate unique ID for accessibility if not provided
const checkboxId =
id || `checkbox-${Math.random().toString(36).substr(2, 9)}`;
const checkboxId = id || `checkbox-${useId()}`;
const accessibilityProps = {
role: "checkbox",
+183
View File
@@ -0,0 +1,183 @@
"use client";
import React, { memo, useCallback, forwardRef, useId } from "react";
const Input = forwardRef(
(
{
size = "medium",
labelVariant = "default",
state = "default",
disabled = false,
error = false,
label,
placeholder,
value,
onChange,
onFocus,
onBlur,
id,
name,
type = "text",
className = "",
...props
},
ref
) => {
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const inputId = id || `input-${generatedId}`;
// Size variants
const sizeStyles = {
small: {
input: "h-[30px] px-[12px] text-[10px]",
label: "text-[12px] leading-[14px] font-medium",
container: "gap-[4px]",
radius: "var(--measures-radius-small)",
},
medium: {
input: "h-[36px] px-[16px] text-[14px] leading-[20px]",
label: "text-[14px] leading-[16px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-medium)",
},
large: {
input: "h-[40px] px-[20px] text-[16px] leading-[24px]",
label: "text-[16px] leading-[20px] font-medium",
container: "gap-[12px]",
radius: "var(--measures-radius-large)",
},
};
// State styles
const getStateStyles = () => {
if (disabled) {
return {
input:
"bg-[var(--color-content-default-secondary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] cursor-not-allowed",
label: "text-[var(--color-content-default-primary)]",
};
}
if (error) {
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-negative)]",
label: "text-[var(--color-content-default-primary)]",
};
}
switch (state) {
case "active":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
};
case "hover":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-[var(--color-border-default-brand-primary)]",
label: "text-[var(--color-content-default-primary)]",
};
case "focus":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
label: "text-[var(--color-content-default-primary)]",
};
default:
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:outline hover:outline-2 hover:outline-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
};
}
};
const stateStyles = getStateStyles();
const currentSize = sizeStyles[size];
// Container classes based on label variant
const containerClasses =
labelVariant === "horizontal"
? `flex items-center gap-[12px]`
: `flex flex-col ${currentSize.container}`;
const labelClasses =
labelVariant === "horizontal"
? `${currentSize.label} font-inter min-w-fit`
: `${currentSize.label} font-inter`;
const inputClasses = `
w-full border transition-all duration-200 ease-in-out
focus:outline-none focus:ring-0
${currentSize.input}
${stateStyles.input}
${className}
`.trim();
const handleChange = useCallback(
(e) => {
if (!disabled && onChange) {
onChange(e);
}
},
[disabled, onChange]
);
const handleFocus = useCallback(
(e) => {
if (!disabled && onFocus) {
onFocus(e);
}
},
[disabled, onFocus]
);
const handleBlur = useCallback(
(e) => {
if (!disabled && onBlur) {
onBlur(e);
}
},
[disabled, onBlur]
);
return (
<div className={containerClasses}>
{label && (
<label
htmlFor={inputId}
className={`${labelClasses} font-inter font-medium`}
style={{ color: "var(--color-content-default-secondary)" }}
>
{label}
</label>
)}
<div className={disabled ? "opacity-40" : ""}>
<input
ref={ref}
id={inputId}
name={name}
type={type}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
className={inputClasses}
style={{ borderRadius: currentSize.radius }}
{...props}
/>
</div>
</div>
);
}
);
Input.displayName = "Input";
export default memo(Input);
+2 -2
View File
@@ -1,6 +1,6 @@
"use client";
import React, { memo, useCallback } from "react";
import React, { memo, useCallback, useId } from "react";
const RadioButton = ({
checked = false,
@@ -71,7 +71,7 @@ const RadioButton = ({
"focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]";
// Generate unique ID for accessibility if not provided
const radioId = id || `radio-${Math.random().toString(36).substr(2, 9)}`;
const radioId = id || `radio-${useId()}`;
const handleToggle = useCallback(
(e) => {
+2 -3
View File
@@ -1,6 +1,6 @@
"use client";
import React, { memo, useCallback } from "react";
import React, { memo, useCallback, useId } from "react";
import RadioButton from "./RadioButton";
const RadioGroup = ({
@@ -15,8 +15,7 @@ const RadioGroup = ({
...props
}) => {
// Generate unique ID for accessibility if not provided
const groupId =
name || `radio-group-${Math.random().toString(36).substr(2, 9)}`;
const groupId = name || `radio-group-${useId()}`;
const handleChange = useCallback(
(optionValue) => {
+85 -30
View File
@@ -3,14 +3,21 @@
import React, { useState } from "react";
import Checkbox from "../components/Checkbox";
import RadioButton from "../components/RadioButton";
import RadioGroup from "../components/RadioGroup";
import Input from "../components/Input";
export default function FormsPlayground() {
const [standardChecked, setStandardChecked] = useState(false);
const [inverseChecked, setInverseChecked] = useState(true);
const [radioValue, setRadioValue] = useState("option1");
const [standardRadioValue, setStandardRadioValue] = useState("option1");
const [inverseRadioValue, setInverseRadioValue] = useState("option2");
const [smallValue, setSmallValue] = useState("Data");
const [mediumValue, setMediumValue] = useState("Data");
const [largeValue, setLargeValue] = useState("Data");
const [defaultLabelValue, setDefaultLabelValue] = useState("Data");
const [horizontalLabelValue, setHorizontalLabelValue] = useState("Data");
const [smallHorizontalValue, setSmallHorizontalValue] = useState("Data");
const [smallDefaultValue, setSmallDefaultValue] = useState("Data");
const [errorStateValue, setErrorStateValue] = useState("Data");
const [disabledStateValue, setDisabledStateValue] = useState("Data");
return (
<div className="p-[24px] space-y-[24px]">
@@ -59,38 +66,86 @@ export default function FormsPlayground() {
</section>
<section className="space-y-[12px]">
<h2 className="font-space text-[18px]">Radio Group</h2>
<h2 className="font-space text-[18px]">Input Examples</h2>
<div className="max-w-[520px] space-y-[16px]">
<div>
<h3 className="font-space text-[14px] mb-[8px]">Standard Mode</h3>
<RadioGroup
name="standard-radio"
value={standardRadioValue}
mode="standard"
state="default"
onChange={({ value }) => setStandardRadioValue(value)}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
<h3 className="font-space text-[14px] mb-[8px]">Sizes</h3>
<div className="space-y-[12px]">
<Input
label="Small"
size="small"
value={smallValue}
onChange={(e) => setSmallValue(e.target.value)}
/>
<Input
label="Medium"
size="medium"
value={mediumValue}
onChange={(e) => setMediumValue(e.target.value)}
/>
<Input
label="Large"
size="large"
value={largeValue}
onChange={(e) => setLargeValue(e.target.value)}
/>
</div>
</div>
<div>
<h3 className="font-space text-[14px] mb-[8px]">Inverse Mode</h3>
<RadioGroup
name="inverse-radio"
value={inverseRadioValue}
mode="inverse"
state="default"
onChange={({ value }) => setInverseRadioValue(value)}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
<h3 className="font-space text-[14px] mb-[8px]">Label Variants</h3>
<div className="space-y-[12px]">
<Input
label="Default (Top Label)"
labelVariant="default"
size="medium"
value={defaultLabelValue}
onChange={(e) => setDefaultLabelValue(e.target.value)}
/>
<Input
label="Small Default"
labelVariant="default"
size="small"
value={smallDefaultValue}
onChange={(e) => setSmallDefaultValue(e.target.value)}
/>
<Input
label="Horizontal (Left Label)"
labelVariant="horizontal"
size="medium"
value={horizontalLabelValue}
onChange={(e) => setHorizontalLabelValue(e.target.value)}
/>
<Input
label="Small Horizontal"
labelVariant="horizontal"
size="small"
value={smallHorizontalValue}
onChange={(e) => setSmallHorizontalValue(e.target.value)}
/>
</div>
</div>
<div>
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
<div className="space-y-[12px]">
<Input
label="Error"
size="medium"
state="default"
error={true}
value={errorStateValue}
onChange={(e) => setErrorStateValue(e.target.value)}
/>
<Input
label="Disabled"
size="medium"
state="default"
disabled={true}
value={disabledStateValue}
onChange={(e) => setDisabledStateValue(e.target.value)}
/>
</div>
</div>
</div>
</section>
+5 -8
View File
@@ -31,15 +31,12 @@
--color-*: initial;
/* Font families */
--font-sans:
var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
--font-display:
var(--font-bricolage-grotesque), ui-sans-serif, system-ui, -apple-system,
--font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system,
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono:
var(--font-space-grotesk), ui-monospace, SFMono-Regular, "SF Mono",
Consolas, "Liberation Mono", Menlo, monospace;
--font-display: var(--font-bricolage-grotesque), ui-sans-serif, system-ui,
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: var(--font-space-grotesk), ui-monospace, SFMono-Regular,
"SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
/* Dimension */
--spacing-scale-000: 0px;
+1
View File
@@ -4,6 +4,7 @@ export default defineConfig({
testDir: "tests",
testMatch: [
"tests/e2e/**/*.spec.{js,ts}",
"tests/e2e/**/*.test.{js,ts}",
"tests/accessibility/**/*.spec.{js,ts}",
],
timeout: 60_000,
+269
View File
@@ -0,0 +1,269 @@
import React from "react";
import Input from "../app/components/Input";
export default {
title: "Forms/Input",
component: Input,
parameters: {
layout: "centered",
},
argTypes: {
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
labelVariant: {
control: { type: "select" },
options: ["default", "horizontal"],
},
state: {
control: { type: "select" },
options: ["default", "active", "hover", "focus", "error", "disabled"],
},
disabled: {
control: { type: "boolean" },
},
error: {
control: { type: "boolean" },
},
label: {
control: { type: "text" },
},
placeholder: {
control: { type: "text" },
},
value: {
control: { type: "text" },
},
},
};
const Template = (args) => <Input {...args} />;
// Default story
export const Default = Template.bind({});
Default.args = {
label: "Default Input",
placeholder: "Enter text...",
size: "medium",
labelVariant: "default",
state: "default",
};
// Size variants
export const Small = Template.bind({});
Small.args = {
label: "Small Input",
placeholder: "Small size",
size: "small",
labelVariant: "default",
state: "default",
};
export const Medium = Template.bind({});
Medium.args = {
label: "Medium Input",
placeholder: "Medium size",
size: "medium",
labelVariant: "default",
state: "default",
};
export const Large = Template.bind({});
Large.args = {
label: "Large Input",
placeholder: "Large size",
size: "large",
labelVariant: "default",
state: "default",
};
// Label variants
export const DefaultLabel = Template.bind({});
DefaultLabel.args = {
label: "Default Label (Top)",
placeholder: "Top label",
size: "medium",
labelVariant: "default",
state: "default",
};
export const HorizontalLabel = Template.bind({});
HorizontalLabel.args = {
label: "Horizontal Label",
placeholder: "Left label",
size: "medium",
labelVariant: "horizontal",
state: "default",
};
// States
export const Active = Template.bind({});
Active.args = {
label: "Active State",
placeholder: "Active input",
size: "medium",
labelVariant: "default",
state: "active",
};
export const Hover = Template.bind({});
Hover.args = {
label: "Hover State",
placeholder: "Hover input",
size: "medium",
labelVariant: "default",
state: "hover",
};
export const Focus = Template.bind({});
Focus.args = {
label: "Focus State",
placeholder: "Focused input",
size: "medium",
labelVariant: "default",
state: "focus",
};
export const Error = Template.bind({});
Error.args = {
label: "Error State",
placeholder: "Error input",
size: "medium",
labelVariant: "default",
state: "default",
error: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
label: "Disabled State",
placeholder: "Disabled input",
size: "medium",
labelVariant: "default",
state: "default",
disabled: true,
};
// Interactive example
export const Interactive = (args) => {
const [value, setValue] = React.useState("");
return (
<div className="space-y-4">
<Input
{...args}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p className="text-sm text-gray-600">Current value: "{value}"</p>
</div>
);
};
Interactive.args = {
label: "Interactive Input",
placeholder: "Type something...",
size: "medium",
labelVariant: "default",
state: "default",
};
// All sizes comparison
export const AllSizes = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Small Size</h3>
<div className="space-y-4">
<Input
label="Small Default"
placeholder="Small with top label"
size="small"
labelVariant="default"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Medium Size</h3>
<div className="space-y-4">
<Input
label="Medium Default"
placeholder="Medium with top label"
size="medium"
labelVariant="default"
/>
<Input
label="Medium Horizontal"
placeholder="Medium with left label"
size="medium"
labelVariant="horizontal"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Large Size</h3>
<div className="space-y-4">
<Input
label="Large Default"
placeholder="Large with top label"
size="large"
labelVariant="default"
/>
<Input
label="Large Horizontal"
placeholder="Large with left label"
size="large"
labelVariant="horizontal"
/>
</div>
</div>
</div>
);
// All states comparison
export const AllStates = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Input States</h3>
<div className="space-y-4">
<Input
label="Default State"
placeholder="Default input"
size="medium"
state="default"
/>
<Input
label="Active State"
placeholder="Active input"
size="medium"
state="active"
/>
<Input
label="Hover State"
placeholder="Hover input"
size="medium"
state="hover"
/>
<Input
label="Focus State"
placeholder="Focused input"
size="medium"
state="focus"
/>
<Input
label="Error State"
placeholder="Error input"
size="medium"
error={true}
/>
<Input
label="Disabled State"
placeholder="Disabled input"
size="medium"
disabled={true}
/>
</div>
</div>
</div>
);
+286
View File
@@ -0,0 +1,286 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Input from "../../app/components/Input";
expect.extend(toHaveNoViolations);
describe("Input Component Accessibility", () => {
test("has no accessibility violations", async () => {
const { container } = render(<Input label="Test input" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when disabled", async () => {
const { container } = render(<Input label="Test input" disabled={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when in error state", async () => {
const { container } = render(<Input label="Test input" error={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations with horizontal label", async () => {
const { container } = render(
<Input label="Test input" labelVariant="horizontal" />
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("associates label with input correctly", () => {
render(<Input label="Test input" />);
const input = screen.getByLabelText("Test input");
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute("type", "text");
});
test("maintains label association with custom ID", () => {
render(<Input id="custom-input" label="Test input" />);
const input = screen.getByLabelText("Test input");
expect(input).toHaveAttribute("id", "custom-input");
});
test("supports keyboard navigation", () => {
render(<Input label="Test input" />);
const input = screen.getByRole("textbox");
// Input should be focusable
input.focus();
expect(input).toHaveFocus();
});
test("supports keyboard activation", () => {
const handleChange = vi.fn();
render(<Input label="Test input" onChange={handleChange} />);
const input = screen.getByRole("textbox");
// Type in the input
fireEvent.change(input, { target: { value: "test" } });
expect(handleChange).toHaveBeenCalled();
});
test("supports Enter key activation", () => {
const handleChange = vi.fn();
render(<Input label="Test input" onChange={handleChange} />);
const input = screen.getByRole("textbox");
// Focus the input first
input.focus();
expect(input).toHaveFocus();
fireEvent.keyDown(input, { key: "Enter" });
// Input should still be focused and ready for typing
expect(input).toHaveFocus();
});
test("supports Space key activation", () => {
const handleChange = vi.fn();
render(<Input label="Test input" onChange={handleChange} />);
const input = screen.getByRole("textbox");
// Focus the input first
input.focus();
expect(input).toHaveFocus();
fireEvent.keyDown(input, { key: " " });
// Input should still be focused and ready for typing
expect(input).toHaveFocus();
});
test("supports Tab navigation", () => {
render(
<div>
<Input label="First input" />
<Input label="Second input" />
</div>
);
const firstInput = screen.getByLabelText("First input");
const secondInput = screen.getByLabelText("Second input");
firstInput.focus();
expect(firstInput).toHaveFocus();
// Use userEvent for more realistic tab navigation
fireEvent.keyDown(firstInput, { key: "Tab", code: "Tab" });
// Note: In a real browser, Tab would move focus, but in tests we need to simulate it
secondInput.focus();
expect(secondInput).toHaveFocus();
});
test("supports Shift+Tab navigation", () => {
render(
<div>
<Input label="First input" />
<Input label="Second input" />
</div>
);
const firstInput = screen.getByLabelText("First input");
const secondInput = screen.getByLabelText("Second input");
secondInput.focus();
expect(secondInput).toHaveFocus();
// Use userEvent for more realistic tab navigation
fireEvent.keyDown(secondInput, { key: "Tab", shiftKey: true, code: "Tab" });
// Note: In a real browser, Shift+Tab would move focus, but in tests we need to simulate it
firstInput.focus();
expect(firstInput).toHaveFocus();
});
test("handles disabled state accessibility", () => {
render(<Input label="Test input" disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toBeDisabled();
expect(input).toHaveAttribute("disabled");
});
test("handles error state accessibility", () => {
render(<Input label="Test input" error={true} />);
const input = screen.getByRole("textbox");
// Error state should still be accessible
expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled();
});
test("supports different input types", () => {
const { rerender } = render(<Input type="email" label="Email" />);
let input = screen.getByRole("textbox");
expect(input).toHaveAttribute("type", "email");
rerender(<Input type="password" label="Password" />);
// Password inputs don't have textbox role, they have textbox role only for text inputs
input = screen.getByLabelText("Password");
expect(input).toHaveAttribute("type", "password");
rerender(<Input type="number" label="Number" />);
input = screen.getByRole("spinbutton");
expect(input).toHaveAttribute("type", "number");
});
test("supports placeholder accessibility", () => {
render(<Input placeholder="Enter your name" />);
const input = screen.getByPlaceholderText("Enter your name");
expect(input).toBeInTheDocument();
});
test("supports value accessibility", () => {
render(<Input value="test value" />);
const input = screen.getByDisplayValue("test value");
expect(input).toBeInTheDocument();
});
test("maintains focus management", () => {
const handleFocus = vi.fn();
const handleBlur = vi.fn();
render(
<Input label="Test input" onFocus={handleFocus} onBlur={handleBlur} />
);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalled();
// Focus the input to ensure it has focus
input.focus();
expect(input).toHaveFocus();
fireEvent.blur(input);
expect(handleBlur).toHaveBeenCalled();
// Manually blur the input to ensure it loses focus
input.blur();
expect(input).not.toHaveFocus();
});
test("supports form association", () => {
render(
<form>
<Input name="test-field" label="Test input" />
</form>
);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("name", "test-field");
});
test("supports ARIA attributes", () => {
render(
<Input
label="Test input"
aria-describedby="help-text"
aria-required="true"
/>
);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("aria-describedby", "help-text");
expect(input).toHaveAttribute("aria-required", "true");
});
test("supports custom ARIA labels", () => {
render(<Input aria-label="Custom input label" />);
const input = screen.getByLabelText("Custom input label");
expect(input).toBeInTheDocument();
});
test("handles multiple inputs without conflicts", () => {
render(
<div>
<Input label="First input" />
<Input label="Second input" />
<Input label="Third input" />
</div>
);
const firstInput = screen.getByLabelText("First input");
const secondInput = screen.getByLabelText("Second input");
const thirdInput = screen.getByLabelText("Third input");
expect(firstInput).toBeInTheDocument();
expect(secondInput).toBeInTheDocument();
expect(thirdInput).toBeInTheDocument();
// Each should have unique IDs
expect(firstInput.id).not.toBe(secondInput.id);
expect(secondInput.id).not.toBe(thirdInput.id);
expect(firstInput.id).not.toBe(thirdInput.id);
});
test("supports screen reader navigation", () => {
render(<Input label="Test input" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test input");
// Label should be associated with input
expect(label).toHaveAttribute("for", input.id);
});
test("handles dynamic label changes", () => {
const { rerender } = render(<Input label="Original label" />);
expect(screen.getByText("Original label")).toBeInTheDocument();
rerender(<Input label="Updated label" />);
expect(screen.getByText("Updated label")).toBeInTheDocument();
expect(screen.queryByText("Original label")).not.toBeInTheDocument();
});
test("supports controlled input behavior", () => {
const handleChange = vi.fn();
render(<Input value="controlled value" onChange={handleChange} />);
const input = screen.getByDisplayValue("controlled value");
fireEvent.change(input, { target: { value: "new value" } });
expect(handleChange).toHaveBeenCalled();
});
});
+308
View File
@@ -0,0 +1,308 @@
import { expect, test } from "@playwright/test";
test.describe("Input Component Storybook", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--default");
});
test("renders default input correctly", async ({ page }) => {
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
await expect(input).toHaveAttribute("type", "text");
});
test("renders with label", async ({ page }) => {
const label = page.getByText("Default Input");
await expect(label).toBeVisible();
});
test("renders with placeholder", async ({ page }) => {
const input = page.getByPlaceholder("Enter text...");
await expect(input).toBeVisible();
});
test("handles text input", async ({ page }) => {
const input = page.getByRole("textbox");
await input.fill("test input");
await expect(input).toHaveValue("test input");
});
test("handles focus and blur", async ({ page }) => {
const input = page.getByRole("textbox");
await input.focus();
await expect(input).toBeFocused();
await input.blur();
await expect(input).not.toBeFocused();
});
test("handles keyboard navigation", async ({ page }) => {
const input = page.getByRole("textbox");
await input.focus();
await expect(input).toBeFocused();
await input.press("Tab");
// Input should lose focus when tabbing away
});
test("handles different input types", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--interactive");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
// Test typing
await input.fill("test@example.com");
await expect(input).toHaveValue("test@example.com");
});
});
test.describe("Input Component - Size Variants", () => {
test("renders small size correctly", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--small");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Small Input");
await expect(label).toBeVisible();
});
test("renders medium size correctly", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--medium");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Medium Input");
await expect(label).toBeVisible();
});
test("renders large size correctly", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--large");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Large Input");
await expect(label).toBeVisible();
});
});
test.describe("Input Component - Label Variants", () => {
test("renders default label variant", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--default-label");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const inputId = await input.getAttribute("id");
const label = page.locator(`label[for="${inputId}"]`);
await expect(label).toBeVisible();
});
test("renders horizontal label variant", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--horizontal-label");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const inputId = await input.getAttribute("id");
const label = page.locator(`label[for="${inputId}"]`);
await expect(label).toBeVisible();
});
});
test.describe("Input Component - States", () => {
test("renders active state", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--active");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Active State");
await expect(label).toBeVisible();
});
test("renders hover state", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--hover");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Hover State");
await expect(label).toBeVisible();
});
test("renders focus state", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--focus");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Focus State");
await expect(label).toBeVisible();
});
test("renders error state", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--error");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
const label = page.getByText("Error State");
await expect(label).toBeVisible();
});
test("renders disabled state", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--disabled");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
await expect(input).toBeDisabled();
const label = page.getByText("Disabled State");
await expect(label).toBeVisible();
});
});
test.describe("Input Component - Comparison Stories", () => {
test("renders all sizes comparison", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--all-sizes");
// Check that all size variants are present
await expect(page.getByText("Small Size")).toBeVisible();
await expect(page.getByText("Medium Size")).toBeVisible();
await expect(page.getByText("Large Size")).toBeVisible();
// Check that inputs are present
const inputs = page.getByRole("textbox");
// Small horizontal story was removed; expect 5 inputs now
await expect(inputs).toHaveCount(5);
});
test("renders all states comparison", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--all-states");
// Check that all state variants are present
await expect(page.getByText("Default State")).toBeVisible();
await expect(page.getByText("Active State")).toBeVisible();
await expect(page.getByText("Hover State")).toBeVisible();
await expect(page.getByText("Focus State")).toBeVisible();
await expect(page.getByText("Error State")).toBeVisible();
await expect(page.getByText("Disabled State")).toBeVisible();
// Check that inputs are present
const inputs = page.getByRole("textbox");
await expect(inputs).toHaveCount(6);
});
});
test.describe("Input Component - Interactive Story", () => {
test("handles interactive input changes", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--interactive");
const input = page.getByRole("textbox");
await expect(input).toBeVisible();
// Test typing
await input.fill("Hello World");
await expect(input).toHaveValue("Hello World");
// Test clearing
await input.fill("");
await expect(input).toHaveValue("");
// Test typing again
await input.fill("New text");
await expect(input).toHaveValue("New text");
});
test("handles focus and blur in interactive story", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--interactive");
const input = page.getByRole("textbox");
await input.focus();
await expect(input).toBeFocused();
await input.blur();
await expect(input).not.toBeFocused();
});
});
test.describe("Input Component - Accessibility", () => {
test("has proper label association", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--default");
const input = page.getByRole("textbox");
const label = page.getByText("Default Input");
await expect(input).toBeVisible();
await expect(label).toBeVisible();
// Check that label is properly associated
const labelFor = await label.getAttribute("for");
const inputId = await input.getAttribute("id");
expect(labelFor).toBe(inputId);
});
test("supports keyboard navigation", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--interactive");
const input = page.getByRole("textbox");
// Focus with keyboard
await input.focus();
await expect(input).toBeFocused();
// Type with keyboard
await input.press("a");
await expect(input).toHaveValue("a");
await input.press("b");
await expect(input).toHaveValue("ab");
});
test("handles disabled state accessibility", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--disabled");
const input = page.getByRole("textbox");
await expect(input).toBeDisabled();
// Verify that filling is not allowed by asserting it remains empty without attempting to fill
await expect(input).toHaveValue("");
});
});
test.describe("Input Component - Form Integration", () => {
test("works within form context", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--interactive");
const input = page.getByRole("textbox");
// Test form-like behavior
await input.fill("form value");
await expect(input).toHaveValue("form value");
// Test clearing
await input.clear();
await expect(input).toHaveValue("");
});
test("handles different input types", async ({ page }) => {
await page.goto("/iframe.html?id=forms-input--interactive");
const input = page.getByRole("textbox");
// Test different input patterns
await input.fill("test@example.com");
await expect(input).toHaveValue("test@example.com");
await input.clear();
await input.fill("12345");
await expect(input).toHaveValue("12345");
});
});
@@ -0,0 +1,426 @@
import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input";
// Test component that uses Input with state management
const TestInputForm = ({ initialValue = "", onValueChange }) => {
const [value, setValue] = useState(initialValue);
const [focused, setFocused] = useState(false);
const handleChange = (e) => {
setValue(e.target.value);
onValueChange?.(e.target.value);
};
const handleFocus = () => setFocused(true);
const handleBlur = () => setFocused(false);
return (
<div>
<Input
label="Test Input"
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
state={focused ? "focus" : "default"}
/>
<div data-testid="value-display">{value}</div>
<div data-testid="focus-status">{focused ? "focused" : "blurred"}</div>
</div>
);
};
// Test component with multiple inputs
const MultiInputForm = () => {
const [values, setValues] = useState({
firstName: "",
lastName: "",
email: "",
});
const handleChange = (field) => (e) => {
setValues((prev) => ({ ...prev, [field]: e.target.value }));
};
return (
<form>
<Input
label="First Name"
name="firstName"
value={values.firstName}
onChange={handleChange("firstName")}
/>
<Input
label="Last Name"
name="lastName"
value={values.lastName}
onChange={handleChange("lastName")}
/>
<Input
label="Email"
name="email"
type="email"
value={values.email}
onChange={handleChange("email")}
/>
</form>
);
};
// Test component with validation
const ValidatedInputForm = () => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
setValue(e.target.value);
setError(e.target.value.length < 3);
};
return (
<div>
<Input
label="Required Field"
value={value}
onChange={handleChange}
error={error}
/>
{error && (
<div data-testid="error-message">Minimum 3 characters required</div>
)}
</div>
);
};
describe("Input Component Integration", () => {
test("handles controlled input with state management", async () => {
const onValueChange = vi.fn();
render(<TestInputForm onValueChange={onValueChange} />);
const input = screen.getByLabelText("Test Input");
const valueDisplay = screen.getByTestId("value-display");
const focusStatus = screen.getByTestId("focus-status");
// Initial state
expect(valueDisplay).toHaveTextContent("");
expect(focusStatus).toHaveTextContent("blurred");
// Type in input
fireEvent.change(input, { target: { value: "test value" } });
expect(valueDisplay).toHaveTextContent("test value");
expect(onValueChange).toHaveBeenCalledWith("test value");
// Focus input
fireEvent.focus(input);
expect(focusStatus).toHaveTextContent("focused");
// Blur input
fireEvent.blur(input);
expect(focusStatus).toHaveTextContent("blurred");
});
test("handles multiple inputs independently", () => {
render(<MultiInputForm />);
const firstNameInput = screen.getByLabelText("First Name");
const lastNameInput = screen.getByLabelText("Last Name");
const emailInput = screen.getByLabelText("Email");
// Type in first input
fireEvent.change(firstNameInput, { target: { value: "John" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("");
expect(emailInput).toHaveValue("");
// Type in second input
fireEvent.change(lastNameInput, { target: { value: "Doe" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("Doe");
expect(emailInput).toHaveValue("");
// Type in third input
fireEvent.change(emailInput, { target: { value: "john@example.com" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("Doe");
expect(emailInput).toHaveValue("john@example.com");
});
test("handles form validation", () => {
render(<ValidatedInputForm />);
const input = screen.getByLabelText("Required Field");
const errorMessage = screen.queryByTestId("error-message");
// Initial state - no error
expect(errorMessage).not.toBeInTheDocument();
// Type short value - should show error
fireEvent.change(input, { target: { value: "ab" } });
expect(screen.getByTestId("error-message")).toBeInTheDocument();
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
// Type longer value - should hide error
fireEvent.change(input, { target: { value: "abc" } });
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
});
test("handles different input types", () => {
render(
<div>
<Input label="Text Input" type="text" />
<Input label="Email Input" type="email" />
<Input label="Password Input" type="password" />
<Input label="Number Input" type="number" />
</div>
);
const textInput = screen.getByLabelText("Text Input");
const emailInput = screen.getByLabelText("Email Input");
const passwordInput = screen.getByLabelText("Password Input");
const numberInput = screen.getByLabelText("Number Input");
expect(textInput).toHaveAttribute("type", "text");
expect(emailInput).toHaveAttribute("type", "email");
expect(passwordInput).toHaveAttribute("type", "password");
expect(numberInput).toHaveAttribute("type", "number");
});
test("handles different sizes and label variants", () => {
render(
<div>
<Input label="Small Default" size="small" labelVariant="default" />
<Input
label="Small Horizontal"
size="small"
labelVariant="horizontal"
/>
<Input label="Medium Default" size="medium" labelVariant="default" />
<Input
label="Medium Horizontal"
size="medium"
labelVariant="horizontal"
/>
<Input label="Large Default" size="large" labelVariant="default" />
<Input
label="Large Horizontal"
size="large"
labelVariant="horizontal"
/>
</div>
);
// All inputs should be present
expect(screen.getByLabelText("Small Default")).toBeInTheDocument();
expect(screen.getByLabelText("Small Horizontal")).toBeInTheDocument();
expect(screen.getByLabelText("Medium Default")).toBeInTheDocument();
expect(screen.getByLabelText("Medium Horizontal")).toBeInTheDocument();
expect(screen.getByLabelText("Large Default")).toBeInTheDocument();
expect(screen.getByLabelText("Large Horizontal")).toBeInTheDocument();
});
test("handles disabled state integration", () => {
const handleChange = vi.fn();
render(
<Input
label="Disabled Input"
disabled={true}
onChange={handleChange}
onFocus={vi.fn()}
onBlur={vi.fn()}
/>
);
const input = screen.getByLabelText("Disabled Input");
// Should be disabled
expect(input).toBeDisabled();
// Should not call handlers
fireEvent.change(input, { target: { value: "test" } });
fireEvent.focus(input);
fireEvent.blur(input);
expect(handleChange).not.toHaveBeenCalled();
});
test("handles error state integration", () => {
render(<Input label="Error Input" error={true} />);
const input = screen.getByLabelText("Error Input");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
expect(input).not.toBeDisabled();
});
test("handles state transitions", async () => {
const TestStateTransitions = () => {
const [state, setState] = useState("default");
return (
<div>
<Input
label="State Test"
state={state}
onFocus={() => setState("focus")}
onBlur={() => setState("default")}
/>
<button onClick={() => setState("hover")}>Set Hover</button>
<button onClick={() => setState("active")}>Set Active</button>
</div>
);
};
render(<TestStateTransitions />);
const input = screen.getByLabelText("State Test");
const hoverButton = screen.getByText("Set Hover");
const activeButton = screen.getByText("Set Active");
// Initial state
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
// Set hover state
fireEvent.click(hoverButton);
expect(input).toHaveClass("border-2");
expect(input).toHaveClass(
"border-[var(--color-border-default-brand-primary)]"
);
// Set active state
fireEvent.click(activeButton);
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
// Focus state
fireEvent.focus(input);
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-info)]"
);
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
test("handles keyboard navigation between inputs", () => {
render(
<div>
<Input label="First Input" />
<Input label="Second Input" />
<Input label="Third Input" />
</div>
);
const firstInput = screen.getByLabelText("First Input");
const secondInput = screen.getByLabelText("Second Input");
const thirdInput = screen.getByLabelText("Third Input");
// Focus first input
firstInput.focus();
expect(firstInput).toHaveFocus();
// Tab to second input - simulate actual tab behavior
fireEvent.keyDown(firstInput, { key: "Tab" });
// Manually focus the second input since tab navigation doesn't work in jsdom
secondInput.focus();
expect(secondInput).toHaveFocus();
// Tab to third input
fireEvent.keyDown(secondInput, { key: "Tab" });
// Manually focus the third input
thirdInput.focus();
expect(thirdInput).toHaveFocus();
// Shift+Tab back to second input
fireEvent.keyDown(thirdInput, { key: "Tab", shiftKey: true });
// Manually focus the second input
secondInput.focus();
expect(secondInput).toHaveFocus();
});
test("handles form submission", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Input label="Test Input" name="testField" />
<button type="submit">Submit</button>
</form>
);
const input = screen.getByLabelText("Test Input");
const submitButton = screen.getByText("Submit");
// Type in input
fireEvent.change(input, { target: { value: "test value" } });
// Submit form
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
test("handles ref forwarding", () => {
const TestRefComponent = () => {
const inputRef = React.useRef();
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} label="Ref Test" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
render(<TestRefComponent />);
const input = screen.getByLabelText("Ref Test");
const focusButton = screen.getByText("Focus Input");
// Click button to focus input via ref
fireEvent.click(focusButton);
expect(input).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const TestDynamicProps = () => {
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState(false);
return (
<div>
<Input label="Dynamic Input" disabled={disabled} error={error} />
<button onClick={() => setDisabled(!disabled)}>
Toggle Disabled
</button>
<button onClick={() => setError(!error)}>Toggle Error</button>
</div>
);
};
render(<TestDynamicProps />);
const input = screen.getByLabelText("Dynamic Input");
const toggleDisabledButton = screen.getByText("Toggle Disabled");
const toggleErrorButton = screen.getByText("Toggle Error");
// Initial state
expect(input).not.toBeDisabled();
expect(input).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
// Toggle disabled
fireEvent.click(toggleDisabledButton);
expect(input).toBeDisabled();
// Toggle error - but first disable the disabled state so error can be tested
fireEvent.click(toggleDisabledButton); // Turn off disabled
fireEvent.click(toggleErrorButton); // Turn on error
// The error state applies the border color through the stateStyles.input class
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
});
});
+269
View File
@@ -0,0 +1,269 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input";
describe("Input Component", () => {
test("renders with default props", () => {
render(<Input />);
const input = screen.getByRole("textbox");
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute("type", "text");
});
test("renders with label", () => {
render(<Input label="Test input" />);
expect(screen.getByText("Test input")).toBeInTheDocument();
expect(screen.getByLabelText("Test input")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<Input placeholder="Enter text..." />);
const input = screen.getByPlaceholderText("Enter text...");
expect(input).toBeInTheDocument();
});
test("renders with value", () => {
render(<Input value="test value" />);
const input = screen.getByDisplayValue("test value");
expect(input).toBeInTheDocument();
});
test("calls onChange when text is entered", () => {
const handleChange = vi.fn();
render(<Input onChange={handleChange} />);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "new text" } });
expect(handleChange).toHaveBeenCalledWith(expect.any(Object));
});
test("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Input onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalledWith(expect.any(Object));
});
test("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Input onBlur={handleBlur} />);
const input = screen.getByRole("textbox");
fireEvent.blur(input);
expect(handleBlur).toHaveBeenCalledWith(expect.any(Object));
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(<Input disabled={true} onChange={handleChange} />);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "new text" } });
expect(handleChange).not.toHaveBeenCalled();
});
test("does not call onFocus when disabled", () => {
const handleFocus = vi.fn();
render(<Input disabled={true} onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).not.toHaveBeenCalled();
});
test("does not call onBlur when disabled", () => {
const handleBlur = vi.fn();
render(<Input disabled={true} onBlur={handleBlur} />);
const input = screen.getByRole("textbox");
fireEvent.blur(input);
expect(handleBlur).not.toHaveBeenCalled();
});
test("applies disabled attributes when disabled", () => {
render(<Input disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toBeDisabled();
});
test("applies correct size classes", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[30px]");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[36px]");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[40px]");
});
test("applies correct label variant classes", () => {
const { rerender } = render(<Input label="Test" labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex-col");
rerender(<Input label="Test" labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center");
});
test("applies error state classes", () => {
render(<Input error={true} />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
});
test("applies disabled state classes", () => {
render(<Input disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("cursor-not-allowed");
expect(input).toHaveClass("bg-[var(--color-content-default-secondary)]");
});
test("applies focus state classes", () => {
render(<Input state="focus" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-info)]"
);
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
test("applies hover state classes", () => {
render(<Input state="hover" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-2");
expect(input).toHaveClass(
"border-[var(--color-border-default-brand-primary)]"
);
});
test("applies active state classes", () => {
render(<Input state="active" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
});
test("applies default state classes", () => {
render(<Input state="default" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass("hover:outline");
});
test("applies custom className", () => {
render(<Input className="custom-class" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("custom-class");
});
test("passes through additional props", () => {
render(<Input id="test-input" name="test" type="email" />);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("id", "test-input");
expect(input).toHaveAttribute("name", "test");
expect(input).toHaveAttribute("type", "email");
});
test("generates unique ID when not provided", () => {
render(<Input label="Test" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test");
expect(input).toHaveAttribute("id");
expect(label).toHaveAttribute("for", input.id);
});
test("uses provided ID when given", () => {
render(<Input id="custom-id" label="Test" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test");
expect(input).toHaveAttribute("id", "custom-id");
expect(label).toHaveAttribute("for", "custom-id");
});
test("applies correct border radius style", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-small)");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-medium)");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-large)");
});
test("applies opacity wrapper when disabled", () => {
render(<Input disabled={true} />);
const wrapper = screen.getByRole("textbox").closest("div");
expect(wrapper).toHaveClass("opacity-40");
});
test("does not apply opacity wrapper when not disabled", () => {
render(<Input disabled={false} />);
const wrapper = screen.getByRole("textbox").closest("div");
expect(wrapper).not.toHaveClass("opacity-40");
});
test("applies correct label styling", () => {
render(<Input label="Test label" size="small" />);
const label = screen.getByText("Test label");
expect(label).toHaveClass("text-[12px]");
expect(label).toHaveClass("leading-[14px]");
expect(label).toHaveClass("font-medium");
expect(label).toHaveStyle("color: var(--color-content-default-secondary)");
});
test("applies correct input text styling for different sizes", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[10px]");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[14px]");
expect(input).toHaveClass("leading-[20px]");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[16px]");
expect(input).toHaveClass("leading-[24px]");
});
test("handles keyboard navigation", () => {
const handleFocus = vi.fn();
render(<Input onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.keyDown(input, { key: "Tab" });
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalled();
});
test("forwards ref correctly", () => {
const ref = React.createRef();
render(<Input ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
test("is memoized", () => {
expect(Input.$$typeof).toBe(Symbol.for("react.memo"));
});
});