Input component with storybook and testing
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user