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
+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"));
});
});