Form Components #22

Merged
an.di merged 10 commits from adilallo/component/FormComponents into main 2025-12-11 04:42:46 +00:00
69 changed files with 18040 additions and 866 deletions
+7
View File
@@ -0,0 +1,7 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
:root {
--font-inter:
"Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
}
+1 -1
View File
@@ -10,7 +10,7 @@ const config = {
"@storybook/addon-a11y",
],
framework: {
name: "@storybook/nextjs-vite",
name: "@storybook/nextjs",
options: {},
},
staticDirs: ["../public"],
+2 -1
View File
@@ -1,4 +1,5 @@
import "../app/globals.css";
import "./fonts.css";
/** @type { import('@storybook/react').Preview } */
const preview = {
@@ -12,7 +13,7 @@ const preview = {
},
decorators: [
(Story) => (
<div className="font-sans">
<div className="font-inter">
<Story />
</div>
),
+168
View File
@@ -0,0 +1,168 @@
"use client";
import React, { memo, useId } from "react";
/**
* Checkbox
* A basic controlled checkbox with visual modes and interaction states.
* This is a minimal first pass; visuals will be refined collaboratively.
*/
const Checkbox = memo(
({
checked = false,
mode = "standard", // "standard" | "inverse"
state = "default", // "default" | "hover" | "focus"
disabled = false,
label,
className = "",
onChange,
id,
name,
value,
ariaLabel,
...props
}) => {
const isInverse = mode === "inverse";
// Base tokens (rough placeholders leveraging existing CSS variables)
const colorSurface = isInverse
? "var(--color-surface-inverse-primary)"
: "var(--color-surface-default-primary)";
const colorContent = isInverse
? "var(--color-content-inverse-primary)"
: "var(--color-content-default-primary)";
const colorBrand = isInverse
? "var(--color-content-inverse-brand-primary)"
: "var(--color-content-default-brand-primary)";
// Visual container depending on state
const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`;
const stateStyles = {
default: "",
hover: "",
focus: "",
};
// Background behavior:
// - Standard: background does not change on check; only checkmark appears
// - Inverse: transparent background, checkmark appears on check
const backgroundWhenChecked = isInverse
? "var(--color-surface-default-transparent)"
: "var(--color-surface-default-primary)";
const checkGlyphColor = checked
? isInverse
? "var(--color-content-inverse-primary)"
: "var(--color-border-default-brand-primary)"
: "transparent";
const labelColor = colorContent;
const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`;
// Force visible outline for standard / default / unchecked
// Outline classes instead of inline styles so hover can override
const defaultOutlineClass = isInverse
? "outline outline-1 outline-[var(--color-border-inverse-primary)]"
: "outline outline-1 outline-[var(--color-border-default-tertiary)]";
// Apply brand outline only on actual :hover, and only when standard/unchecked
const conditionalHoverOutlineClass =
"hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]";
// Focus state for standard/unchecked with brand primary color and specific blur/spread
const conditionalFocusClass =
"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)]";
const handleToggle = (e) => {
if (disabled) return;
onChange?.({
checked: !checked,
value,
event: e,
});
};
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const checkboxId = id || `checkbox-${generatedId}`;
const accessibilityProps = {
role: "checkbox",
"aria-checked": checked ? "true" : "false",
...(disabled && { "aria-disabled": "true", tabIndex: -1 }),
...(!disabled && { tabIndex: 0 }),
...(ariaLabel && { "aria-label": ariaLabel }),
...(label && !ariaLabel && { "aria-labelledby": `${checkboxId}-label` }),
id: checkboxId,
...props,
};
return (
<label
className={`inline-flex items-center gap-[8px] cursor-pointer select-none ${
disabled ? "opacity-60 cursor-not-allowed" : ""
} ${className}`}
onMouseDown={(e) => e.preventDefault()}
>
<span
{...accessibilityProps}
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
handleToggle(e);
}
}}
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`}
style={{
backgroundColor: backgroundWhenChecked,
}}
>
{/* Simple check glyph */}
<svg
width="16"
height="16"
viewBox="0 0 12 12"
aria-hidden="true"
focusable="false"
>
<polyline
points="2.5 6 5 8.5 10 3.5"
stroke={checkGlyphColor}
strokeWidth="1.25"
fill="none"
strokeLinecap="square"
strokeLinejoin="miter"
vectorEffect="non-scaling-stroke"
/>
</svg>
</span>
{label && (
<span
id={`${checkboxId}-label`}
className="font-inter text-[14px] leading-[18px]"
style={{ color: labelColor }}
>
{label}
</span>
)}
{/* Hidden native input for form compatibility (optional for now) */}
<input
type="checkbox"
name={name}
value={value}
checked={checked}
onChange={() => {}}
tabIndex={-1}
aria-hidden="true"
className="sr-only"
readOnly
/>
</label>
);
},
);
Checkbox.displayName = "Checkbox";
export default Checkbox;
+36
View File
@@ -0,0 +1,36 @@
"use client";
import React, { forwardRef, memo } from "react";
const ContextMenu = forwardRef(
({ className = "", children, ...props }, ref) => {
const menuClasses = `
bg-black
border border-[var(--color-border-default-tertiary)]
rounded-[var(--measures-radius-medium)]
shadow-lg
p-[4px]
min-w-[200px]
max-w-[300px]
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div
ref={ref}
className={menuClasses}
role="menu"
style={{ backgroundColor: "#000000" }}
{...props}
>
{children}
</div>
);
},
);
ContextMenu.displayName = "ContextMenu";
export default memo(ContextMenu);
+21
View File
@@ -0,0 +1,21 @@
"use client";
import React, { forwardRef, memo } from "react";
const ContextMenuDivider = forwardRef(({ className = "", ...props }, ref) => {
const dividerClasses = `
border-t border-[var(--color-border-default-tertiary)]
my-1
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div ref={ref} className={dividerClasses} role="separator" {...props} />
);
});
ContextMenuDivider.displayName = "ContextMenuDivider";
export default memo(ContextMenuDivider);
+127
View File
@@ -0,0 +1,127 @@
"use client";
import React, { forwardRef, memo, useCallback } from "react";
const ContextMenuItem = forwardRef(
(
{
children,
selected = false,
hasSubmenu = false,
disabled = false,
className = "",
onClick,
size = "medium",
...props
},
ref,
) => {
const getTextSize = () => {
switch (size) {
case "small":
return "text-[10px] leading-[14px]";
case "medium":
return "text-[14px] leading-[20px]";
case "large":
return "text-[16px] leading-[24px]";
default:
return "text-[14px] leading-[20px]";
}
};
const itemClasses = `
flex items-center justify-between
px-[8px] py-[4px]
text-[var(--color-content-default-brand-primary)]
${getTextSize()}
cursor-pointer
transition-colors duration-150
${
selected
? "bg-[var(--color-surface-default-secondary)] rounded-[var(--measures-radius-small)]"
: ""
}
${
disabled
? "opacity-50 cursor-not-allowed"
: "hover:!bg-[var(--color-surface-default-secondary)] hover:!rounded-[var(--measures-radius-small)]"
}
${className}
`
.trim()
.replace(/\s+/g, " ");
const handleClick = useCallback(
(e) => {
if (!disabled && onClick) {
onClick(e);
}
},
[disabled, onClick],
);
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (!disabled && onClick) {
onClick(e);
}
}
},
[disabled, onClick],
);
return (
<div
ref={ref}
className={itemClasses}
role="menuitem"
tabIndex={disabled ? -1 : 0}
aria-current={selected ? "true" : undefined}
aria-disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
>
<div className="flex items-center gap-[8px]">
{selected && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
<span>{children}</span>
</div>
{hasSubmenu && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
)}
</div>
);
},
);
ContextMenuItem.displayName = "ContextMenuItem";
export default memo(ContextMenuItem);
+30
View File
@@ -0,0 +1,30 @@
"use client";
import React, { forwardRef, memo } from "react";
const ContextMenuSection = forwardRef(
({ title, children, className = "", ...props }, ref) => {
const sectionClasses = `
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div ref={ref} className={sectionClasses} role="group" {...props}>
{title && (
<div className="px-3 py-2">
<div className="text-[var(--color-content-default-primary)] text-sm font-medium">
{title}
</div>
</div>
)}
{children}
</div>
);
},
);
ContextMenuSection.displayName = "ContextMenuSection";
export default memo(ContextMenuSection);
+185
View File
@@ -0,0 +1,185 @@
"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:
labelVariant === "horizontal"
? "h-[30px] px-[12px] py-[8px] text-[10px]"
: "h-[32px] px-[12px] py-[8px] text-[10px]",
label: "text-[12px] leading-[14px] font-medium",
container: "gap-[4px]",
radius: "var(--measures-radius-small)",
},
medium: {
input: "h-[36px] px-[12px] py-[8px] text-[14px] leading-[20px]",
label: "text-[14px] leading-[16px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-medium)",
},
large: {
input: "h-[40px] px-[12px] py-[8px] 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-secondary)]",
};
}
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-secondary)]",
};
}
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-secondary)]",
};
case "hover":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
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-secondary)]",
};
default:
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
};
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 text-[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);
+149
View File
@@ -0,0 +1,149 @@
"use client";
import React, { memo, useCallback, useId } from "react";
const RadioButton = ({
checked = false,
mode = "standard",
state = "default",
disabled = false,
label,
onChange,
id,
name,
value,
ariaLabel,
className = "",
...props
}) => {
const isInverse = mode === "inverse";
// Base tokens (using same design tokens as Checkbox)
const colorSurface = isInverse
? "var(--color-surface-inverse-primary)"
: "var(--color-surface-default-primary)";
const colorContent = isInverse
? "var(--color-content-inverse-primary)"
: "var(--color-content-default-primary)";
const colorBrand = isInverse
? "var(--color-content-inverse-brand-primary)"
: "var(--color-content-default-brand-primary)";
// Visual container depending on state
const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`;
const stateStyles = {
default: "",
hover: "",
focus: "",
};
// Background behavior:
// - Standard: background does not change on check; only dot appears
// - Inverse: transparent background, dot appears on check
const backgroundWhenChecked = isInverse
? "var(--color-surface-default-transparent)"
: "var(--color-surface-default-primary)";
// Dot color for selected state
const dotColor = checked
? isInverse
? "var(--color-content-inverse-primary)"
: "var(--color-border-default-brand-primary)"
: "transparent";
const labelColor = colorContent;
const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`;
// Force visible outline for standard / default / unchecked
const defaultOutlineClass = isInverse
? "outline outline-1 outline-[var(--color-border-inverse-primary)]"
: "outline outline-1 outline-[var(--color-border-default-tertiary)]";
// Apply brand outline only on actual :hover
// Standard mode uses default brand primary, inverse mode uses inverse brand primary
const conditionalHoverOutlineClass = isInverse
? "hover:outline hover:outline-1 hover:outline-[var(--color-border-inverse-brand-primary)]"
: "hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]";
// Focus state for standard/unchecked with brand primary color and specific blur/spread
const conditionalFocusClass =
"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 generatedId = useId();
const radioId = id || `radio-${generatedId}`;
const handleToggle = useCallback(
(e) => {
if (!disabled && onChange && !checked) {
onChange({ checked: true, value });
}
},
[disabled, onChange, checked, value],
);
return (
<label
className={`inline-flex items-center gap-[8px] cursor-pointer select-none ${
disabled ? "opacity-60 cursor-not-allowed" : ""
} ${className}`}
onMouseDown={(e) => e.preventDefault()}
onClick={handleToggle}
>
<span
onKeyDown={(e) => {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
handleToggle(e);
}
}}
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`}
style={{
backgroundColor: backgroundWhenChecked,
}}
tabIndex={0}
role="radio"
aria-checked={checked ? "true" : "false"}
{...(disabled && { "aria-disabled": "true" })}
{...(ariaLabel && { "aria-label": ariaLabel })}
{...(label && !ariaLabel && { "aria-labelledby": `${radioId}-label` })}
id={radioId}
>
{/* Radio dot */}
<div
className="w-[16px] h-[16px] rounded-full transition-all duration-200"
style={{
backgroundColor: dotColor,
}}
/>
</span>
{label && (
<span
id={`${radioId}-label`}
className="font-inter text-[14px] leading-[18px]"
style={{ color: labelColor }}
>
{label}
</span>
)}
{/* Hidden input for form submission */}
<input
type="radio"
name={name}
value={value}
checked={checked}
onChange={() => {}}
disabled={disabled}
className="sr-only"
tabIndex={-1}
aria-hidden="true"
{...props}
/>
</label>
);
};
RadioButton.displayName = "RadioButton";
export default memo(RadioButton);
+65
View File
@@ -0,0 +1,65 @@
"use client";
import React, { memo, useCallback, useId } from "react";
import RadioButton from "./RadioButton";
const RadioGroup = ({
name,
value,
onChange,
mode = "standard",
state = "default",
disabled = false,
options = [],
className = "",
...props
}) => {
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `radio-group-${generatedId}`;
const handleChange = useCallback(
(optionValue) => {
if (!disabled && onChange) {
onChange({ value: optionValue });
}
},
[disabled, onChange],
);
return (
<div
className={`space-y-[8px] ${className}`}
role="radiogroup"
aria-label={props["aria-label"]}
{...props}
>
{options.map((option, index) => {
const isSelected = value === option.value;
return (
<RadioButton
key={option.value}
checked={isSelected}
mode={mode}
state={state}
disabled={disabled}
label={option.label}
name={groupId}
value={option.value}
ariaLabel={option.ariaLabel}
onChange={({ checked }) => {
if (checked) {
handleChange(option.value);
}
}}
/>
);
})}
</div>
);
};
RadioGroup.displayName = "RadioGroup";
export default memo(RadioGroup);
+340
View File
@@ -0,0 +1,340 @@
"use client";
import React, {
forwardRef,
useId,
useState,
useRef,
useEffect,
useCallback,
memo,
} from "react";
import SelectDropdown from "./SelectDropdown";
import SelectOption from "./SelectOption";
const Select = forwardRef(
(
{
id,
label,
labelVariant = "default",
size = "medium",
state = "default",
disabled = false,
error = false,
placeholder = "Select an option",
className = "",
children,
value,
onChange,
...props
},
ref,
) => {
const generatedId = useId();
const selectId = id || `select-${generatedId}`;
const labelId = `${selectId}-label`;
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(value || "");
const selectRef = useRef(null);
const menuRef = useRef(null);
// Handle click outside to close menu
useEffect(() => {
const handleClickOutside = (event) => {
if (
menuRef.current &&
!menuRef.current.contains(event.target) &&
selectRef.current &&
!selectRef.current.contains(event.target)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen]);
// Handle option selection
const handleOptionSelect = useCallback(
(optionValue, optionText) => {
setSelectedValue(optionValue);
setIsOpen(false);
if (onChange) {
onChange({ target: { value: optionValue, text: optionText } });
}
// Return focus to the select button for accessibility
if (selectRef.current) {
selectRef.current.focus();
}
},
[onChange],
);
// Handle select button click
const handleSelectClick = useCallback(() => {
if (!disabled) {
setIsOpen(!isOpen);
}
}, [disabled, isOpen]);
// Handle keyboard navigation
const handleKeyDown = useCallback(
(e) => {
if (disabled) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setIsOpen(!isOpen);
} else if (e.key === "Escape") {
setIsOpen(false);
}
},
[disabled, isOpen],
);
const getSizeStyles = () => {
const baseStyles = "w-full";
switch (size) {
case "small":
const smallHeight =
labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]";
return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`;
case "medium":
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
case "large":
return `${baseStyles} h-[40px] pl-[12px] pr-[40px] py-[8px] text-[16px] leading-[24px]`;
default:
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
}
};
const getLabelSizeStyles = () => {
switch (size) {
case "small":
return "text-[12px] leading-[14px]";
case "medium":
return "text-[14px] leading-[16px]";
case "large":
return "text-[16px] leading-[20px]";
default:
return "text-[14px] leading-[16px]";
}
};
const getStateStyles = () => {
if (disabled) {
return {
select:
"bg-[var(--color-content-default-secondary)] border-[var(--color-border-default-tertiary)] cursor-not-allowed opacity-40",
label: "text-[var(--color-content-default-secondary)]",
};
}
if (error) {
return {
select: "border-[var(--color-border-default-utility-negative)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
switch (state) {
case "hover":
return {
select:
"border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
select:
"border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
label: "text-[var(--color-content-default-secondary)]",
};
default:
return {
select: "border-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
};
const getBorderRadius = () => {
switch (size) {
case "small":
return "rounded-[var(--measures-radius-small)]";
case "medium":
return "rounded-[var(--measures-radius-medium)]";
case "large":
return "rounded-[var(--measures-radius-large)]";
default:
return "rounded-[var(--measures-radius-medium)]";
}
};
const sizeStyles = getSizeStyles();
const labelSizeStyles = getLabelSizeStyles();
const stateStyles = getStateStyles();
const borderRadius = getBorderRadius();
const selectClasses = `
${sizeStyles}
${stateStyles.select}
${borderRadius}
bg-[var(--color-background-default-primary)]
text-[var(--color-content-default-primary)]
border
font-inter
font-normal
appearance-none
cursor-pointer
transition-all
duration-200
focus:outline-none
focus-visible:border focus-visible:border-[var(--color-border-default-utility-info)] focus-visible:shadow-[0_0_5px_3px_#3281F8]
text-left
justify-start
hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]
${className}
`
.trim()
.replace(/\s+/g, " ");
const labelClasses = `
${labelSizeStyles}
${stateStyles.label}
font-inter
font-medium
block
mb-[4px]
`
.trim()
.replace(/\s+/g, " ");
const containerClasses =
labelVariant === "horizontal"
? "flex items-center gap-[12px]"
: "flex flex-col";
// Get display text for selected value
const getDisplayText = () => {
if (!selectedValue) return placeholder;
// Handle options prop
if (props.options && Array.isArray(props.options)) {
const selectedOption = props.options.find(
(option) => option.value === selectedValue,
);
return selectedOption ? selectedOption.label : placeholder;
}
// Handle children (option elements)
const selectedOption = React.Children.toArray(children).find(
(child) => child.props.value === selectedValue,
);
return selectedOption ? selectedOption.props.children : placeholder;
};
return (
<div className={containerClasses}>
{label && (
<label
id={labelId}
htmlFor={selectId}
className={`${labelClasses} text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
)}
<div className="relative">
<button
ref={selectRef}
id={selectId}
disabled={disabled}
className={selectClasses}
aria-labelledby={label ? labelId : undefined}
aria-invalid={error}
aria-expanded={isOpen}
aria-haspopup="listbox"
onClick={handleSelectClick}
onKeyDown={handleKeyDown}
{...props}
>
<span className="text-left">{getDisplayText()}</span>
</button>
<div className="absolute inset-y-0 right-0 flex items-center pr-[12px] pointer-events-none">
<svg
className={`${
size === "large" ? "w-5 h-5" : "w-4 h-4"
} text-[var(--color-content-default-primary)] transition-transform duration-200 ${
isOpen ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
{isOpen && (
<div
ref={menuRef}
className="absolute top-full left-0 right-0 z-50 mt-1"
>
<SelectDropdown>
{props.options && Array.isArray(props.options)
? props.options.map((option) => (
<SelectOption
key={option.value}
selected={option.value === selectedValue}
size={size}
onClick={() =>
handleOptionSelect(option.value, option.label)
}
>
{option.label}
</SelectOption>
))
: React.Children.map(children, (child) => {
if (child.type === "option") {
return (
<SelectOption
key={child.props.value}
selected={child.props.value === selectedValue}
size={size}
onClick={() =>
handleOptionSelect(
child.props.value,
child.props.children,
)
}
>
{child.props.children}
</SelectOption>
);
}
return child;
})}
</SelectDropdown>
</div>
)}
</div>
</div>
);
},
);
Select.displayName = "Select";
export default memo(Select);
+37
View File
@@ -0,0 +1,37 @@
"use client";
import React, { forwardRef, memo } from "react";
const SelectDropdown = forwardRef(
({ className = "", children, ...props }, ref) => {
const menuClasses = `
bg-black
border border-[var(--color-border-default-tertiary)]
rounded-[var(--measures-radius-medium)]
shadow-lg
p-[4px]
min-w-[200px]
max-w-[300px]
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div
ref={ref}
className={menuClasses}
role="listbox"
aria-label="Select an option"
style={{ backgroundColor: "#000000" }}
{...props}
>
{children}
</div>
);
},
);
SelectDropdown.displayName = "SelectDropdown";
export default memo(SelectDropdown);
+111
View File
@@ -0,0 +1,111 @@
"use client";
import React, { forwardRef, memo, useCallback } from "react";
const SelectOption = forwardRef(
(
{
children,
selected = false,
disabled = false,
className = "",
onClick,
size = "medium",
...props
},
ref,
) => {
const getTextSize = () => {
switch (size) {
case "small":
return "text-[10px] leading-[14px]";
case "medium":
return "text-[14px] leading-[20px]";
case "large":
return "text-[16px] leading-[24px]";
default:
return "text-[14px] leading-[20px]";
}
};
const itemClasses = `
flex items-center justify-between
px-[8px] py-[4px]
text-[var(--color-content-default-brand-primary)]
${getTextSize()}
cursor-pointer
transition-colors duration-150
${
selected
? "bg-[var(--color-surface-default-secondary)] rounded-[var(--measures-radius-small)]"
: ""
}
${
disabled
? "opacity-50 cursor-not-allowed"
: "hover:!bg-[var(--color-surface-default-secondary)] hover:!rounded-[var(--measures-radius-small)]"
}
${className}
`
.trim()
.replace(/\s+/g, " ");
const handleClick = useCallback(
(e) => {
if (!disabled && onClick) {
onClick(e);
}
},
[disabled, onClick],
);
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (!disabled && onClick) {
onClick(e);
}
}
},
[disabled, onClick],
);
return (
<div
ref={ref}
className={itemClasses}
role="option"
tabIndex={disabled ? -1 : 0}
aria-selected={selected}
aria-disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
>
<div className="flex items-center gap-[8px]">
{selected && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
<span>{children}</span>
</div>
</div>
);
},
);
SelectOption.displayName = "SelectOption";
export default memo(SelectOption);
+163
View File
@@ -0,0 +1,163 @@
import React, { memo, useCallback, useId, forwardRef } from "react";
const Switch = memo(
forwardRef((props, ref) => {
const {
checked = false,
onChange,
onFocus,
onBlur,
state = "default",
label,
className = "",
...rest
} = props;
const switchId = useId();
const handleClick = useCallback(
(e) => {
if (onChange) {
onChange(e);
}
},
[onChange],
);
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (onChange) {
onChange(e);
}
}
},
[onChange],
);
const handleFocus = useCallback(
(e) => {
if (onFocus) {
onFocus(e);
}
},
[onFocus],
);
const handleBlur = useCallback(
(e) => {
if (onBlur) {
onBlur(e);
}
},
[onBlur],
);
// Switch track styles based on checked state
const getTrackStyles = useCallback(() => {
return checked
? "bg-[var(--color-surface-inverse-tertiary)]"
: "bg-[var(--color-surface-default-tertiary)]";
}, [checked]);
// Switch thumb styles based on checked state
const getThumbStyles = useCallback(() => {
return "bg-[var(--color-gray-000)]";
}, []);
// Focus styles
const getFocusStyles = useCallback(() => {
if (state === "focus") {
return "shadow-[0_0_5px_3px_#3281F8] rounded-full";
}
return "";
}, [state]);
const trackStyles = getTrackStyles();
const thumbStyles = getThumbStyles();
const focusStyles = getFocusStyles();
const switchClasses = `
relative
inline-flex
items-center
cursor-pointer
transition-all
duration-200
focus:outline-none
focus-visible:shadow-[0_0_5px_3px_#3281F8]
focus-visible:rounded-full
${focusStyles}
${className}
`
.trim()
.replace(/\s+/g, " ");
const trackClasses = `
${trackStyles}
w-[40px]
h-[24px]
rounded-full
transition-all
duration-200
flex
items-center
${checked ? "justify-end" : "justify-start"}
p-[2px]
`
.trim()
.replace(/\s+/g, " ");
const thumbClasses = `
${thumbStyles}
w-[var(--measures-sizing-020)]
h-[var(--measures-sizing-020)]
rounded-[var(--measures-radius-xlarge)]
transition-all
duration-200
shadow-sm
`
.trim()
.replace(/\s+/g, " ");
const labelClasses = `
ml-[var(--measures-spacing-008)]
font-inter
font-normal
text-[14px]
leading-[20px]
text-[var(--color-content-default-primary)]
`
.trim()
.replace(/\s+/g, " ");
return (
<div className="flex items-center">
<button
ref={ref}
id={switchId}
type="button"
role="switch"
aria-checked={checked}
aria-label={label || "Toggle switch"}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
className={switchClasses}
{...rest}
>
<div className={trackClasses}>
<div className={thumbClasses} />
</div>
</button>
{label && <span className={labelClasses}>{label}</span>}
</div>
);
}),
);
Switch.displayName = "Switch";
export default Switch;
+190
View File
@@ -0,0 +1,190 @@
"use client";
import React, { memo, useCallback, forwardRef, useId } from "react";
const TextArea = forwardRef(
(
{
size = "medium",
labelVariant = "default",
state = "default",
disabled = false,
error = false,
label,
placeholder,
value,
onChange,
onFocus,
onBlur,
id,
name,
className = "",
rows,
...props
},
ref,
) => {
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const textareaId = id || `textarea-${generatedId}`;
// Size variants with specific heights and radius for TextArea
const sizeStyles = {
small: {
textarea:
labelVariant === "horizontal"
? "h-[60px] px-[12px] py-[8px] text-[10px]"
: "h-[60px] px-[12px] py-[8px] text-[10px]",
label: "text-[12px] leading-[14px] font-medium",
container: "gap-[4px]",
radius: "var(--measures-radius-xsmall)",
},
medium: {
textarea:
labelVariant === "horizontal"
? "h-[110px] px-[12px] py-[8px] text-[14px] leading-[20px]"
: "h-[100px] px-[12px] py-[8px] text-[14px] leading-[20px]",
label: "text-[14px] leading-[16px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-xsmall)",
},
large: {
textarea: "h-[150px] px-[12px] py-[8px] text-[16px] leading-[24px]",
label: "text-[16px] leading-[20px] font-medium",
container: "gap-[12px]",
radius: "var(--measures-radius-small)",
},
};
// State styles
const getStateStyles = () => {
if (disabled) {
return {
textarea:
"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-secondary)]",
};
}
if (error) {
return {
textarea:
"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-secondary)]",
};
}
switch (state) {
case "active":
return {
textarea:
"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-secondary)]",
};
case "hover":
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
textarea:
"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-secondary)]",
};
default:
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
};
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 textareaClasses = `
w-full border transition-all duration-200 ease-in-out
focus:outline-none focus:ring-0 resize-none
${currentSize.textarea}
${stateStyles.textarea}
${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={textareaId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
)}
<div className={disabled ? "opacity-40" : ""}>
<textarea
ref={ref}
id={textareaId}
name={name}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
rows={rows}
className={textareaClasses}
style={{ borderRadius: currentSize.radius }}
aria-disabled={disabled}
aria-invalid={error}
{...props}
/>
</div>
</div>
);
},
);
TextArea.displayName = "TextArea";
export default memo(TextArea);
+194
View File
@@ -0,0 +1,194 @@
import React, { memo, useCallback, useId, forwardRef } from "react";
const Toggle = forwardRef(
(
{
label,
checked = false,
onChange,
onFocus,
onBlur,
disabled = false,
state = "default",
showIcon = false,
showText = false,
icon = "I",
text = "Toggle",
className = "",
...props
},
ref,
) => {
const toggleId = useId();
const labelId = useId();
// Size styles - single size with specific dimensions
const sizeStyles = {
toggle: "h-[var(--measures-sizing-032)] px-[16px] py-[8px] gap-[4px]",
label: "text-[12px] leading-[16px]",
};
// State styles
const getStateStyles = () => {
if (disabled) {
return {
toggle:
"bg-[var(--color-surface-default-tertiary)] text-[var(--color-content-default-tertiary)] cursor-not-allowed",
label: "text-[var(--color-content-default-secondary)]",
};
}
if (checked) {
switch (state) {
case "hover":
return {
toggle:
"bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
toggle:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]",
label: "text-[var(--color-content-default-secondary)]",
};
default:
return {
toggle:
"bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)] shadow-[0_0_0_1px_var(--color-border-default-brand-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
} else {
switch (state) {
case "hover":
return {
toggle:
"bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
toggle:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]",
label: "text-[var(--color-content-default-secondary)]",
};
default:
return {
toggle:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
}
};
const stateStyles = getStateStyles();
const currentSize = sizeStyles;
// Container classes
const containerClasses = "flex flex-col gap-[4px]";
const labelClasses = `${currentSize.label} font-inter font-medium`;
const toggleClasses = `
${currentSize.toggle}
${stateStyles.toggle}
rounded-full
font-inter
font-normal
text-[12px]
leading-[16px]
cursor-pointer
transition-all
duration-200
focus:outline-none
focus-visible:shadow-[0_0_5px_1px_#3281F8]
${!checked ? "hover:!bg-[var(--color-surface-default-secondary)]" : ""}
flex
items-center
justify-center
gap-[4px]
${className}
`
.trim()
.replace(/\s+/g, " ");
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],
);
const handleKeyDown = useCallback(
(e) => {
if (!disabled && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
if (onChange) {
onChange(e);
}
}
},
[disabled, onChange],
);
return (
<div className={containerClasses}>
{label && (
<label
id={labelId}
htmlFor={toggleId}
className={`${labelClasses} text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
)}
<div className={disabled ? "opacity-40" : ""}>
<button
ref={ref}
id={toggleId}
type="button"
role="switch"
aria-checked={checked}
aria-labelledby={label ? labelId : undefined}
disabled={disabled}
onClick={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
className={toggleClasses}
{...props}
>
{showIcon && <span className="italic">{icon}</span>}
{showText && <span>{text}</span>}
</button>
</div>
</div>
);
},
);
Toggle.displayName = "Toggle";
export default memo(Toggle);
+137
View File
@@ -0,0 +1,137 @@
import React, { memo, useCallback, useId, forwardRef } from "react";
const ToggleGroup = memo(
forwardRef((props, ref) => {
const {
children,
className = "",
position = "left",
state = "default",
showText = true,
ariaLabel,
onChange,
onFocus,
onBlur,
...rest
} = props;
const groupId = useId();
// Position-based styling for border radius
const getPositionStyles = useCallback((pos) => {
switch (pos) {
case "left":
return "rounded-l-[var(--measures-radius-medium)] rounded-r-none";
case "middle":
return "rounded-none";
case "right":
return "rounded-r-[var(--measures-radius-medium)] rounded-l-none";
default:
return "rounded-[var(--measures-radius-medium)]";
}
}, []);
// State-based styling
const getStateStyles = useCallback((state) => {
switch (state) {
case "hover":
return "bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)]";
case "focus":
return "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]";
case "selected":
return "bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)] shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]";
case "default":
default:
return "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)]";
}
}, []);
const positionStyles = getPositionStyles(position);
const stateStyles = getStateStyles(state);
const handleClick = useCallback(
(e) => {
if (onChange) {
onChange(e);
}
},
[onChange],
);
const handleFocus = useCallback(
(e) => {
if (onFocus) {
onFocus(e);
}
},
[onFocus],
);
const handleBlur = useCallback(
(e) => {
if (onBlur) {
onBlur(e);
}
},
[onBlur],
);
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (onChange) {
onChange(e);
}
}
},
[onChange],
);
const toggleClasses = `
${positionStyles}
${stateStyles}
py-[var(--measures-spacing-008)]
px-[var(--measures-spacing-008)]
gap-[var(--measures-spacing-008)]
font-inter
font-medium
text-[12px]
leading-[12px]
cursor-pointer
transition-all
duration-200
focus:outline-none
focus-visible:shadow-[0_0_5px_1px_#3281F8]
hover:bg-[var(--color-magenta-magenta100)]
flex
items-center
justify-center
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<button
ref={ref}
id={groupId}
type="button"
role="button"
aria-label={ariaLabel || (showText ? undefined : "Toggle option")}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
className={toggleClasses}
{...rest}
>
{showText ? children : children || "☰"}
</button>
);
}),
);
ToggleGroup.displayName = "ToggleGroup";
export default ToggleGroup;
+5251 -850
View File
File diff suppressed because it is too large Load Diff
+10 -13
View File
@@ -17,7 +17,7 @@
"test": "vitest run --coverage",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:sb": "storybook dev -p 6006 & wait-on http://localhost:6006 && test-storybook",
"test:sb": "storybook dev -p 6006 & wait-on http://localhost:6006 && test-storybook --url http://localhost:6006",
"e2e": "playwright test",
"e2e:ui": "playwright test --ui",
"e2e:performance": "playwright test tests/e2e/performance.spec.ts",
@@ -47,6 +47,7 @@
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^15.5.2",
"ajv": "^8.12.0",
"critters": "^0.0.23",
"gray-matter": "^4.0.3",
"next": "15.2.4",
@@ -58,18 +59,14 @@
"@eslint/eslintrc": "^3",
"@lhci/cli": "^0.15.1",
"@playwright/test": "^1.55.0",
"@storybook/addon-a11y": "^9.1.2",
"@storybook/addon-a11y": "^8.3.0",
"@storybook/addon-actions": "^9.0.8",
"@storybook/addon-docs": "^9.1.2",
"@storybook/addon-essentials": "^9.0.0-alpha.12",
"@storybook/addon-interactions": "^9.0.0-alpha.10",
"@storybook/addon-onboarding": "^9.1.2",
"@storybook/addon-styling-webpack": "^2.0.0",
"@storybook/addon-docs": "^8.3.0",
"@storybook/addon-essentials": "^8.3.0",
"@storybook/addon-interactions": "^8.3.0",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/addon-vitest": "^9.1.2",
"@storybook/nextjs-vite": "^9.1.2",
"@storybook/test": "^9.0.0-alpha.2",
"@storybook/test-runner": "^0.23.0",
"@storybook/nextjs": "^8.3.0",
"@storybook/test-runner": "^0.22.1",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4.1.11",
"@testing-library/jest-dom": "^6.8.0",
@@ -84,14 +81,14 @@
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9",
"eslint-config-next": "15.2.0",
"eslint-plugin-storybook": "^9.1.2",
"eslint-plugin-storybook": "^9.0.7",
"jest-axe": "^10.0.0",
"jsdom": "^26.1.0",
"msw": "^2.10.5",
"playwright": "^1.54.2",
"postcss": "^8.5.6",
"start-server-and-test": "^2.0.13",
"storybook": "^9.1.2",
"storybook": "^8.3.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4",
+1
View File
@@ -4,6 +4,7 @@ export default defineConfig({
testDir: "tests",
testMatch: [
"tests/e2e/**/*.spec.{js,ts}",
"tests/e2e/**/*.test.{js,ts}",
"tests/accessibility/**/*.spec.{js,ts}",
],
timeout: 60_000,
+152
View File
@@ -0,0 +1,152 @@
import React from "react";
import Checkbox from "../app/components/Checkbox";
import {
DefaultInteraction,
CheckedInteraction,
StandardInteraction,
InverseInteraction,
KeyboardInteraction,
AccessibilityInteraction,
FormIntegration,
} from "../tests/storybook/Checkbox.interactions.test";
export default {
title: "Forms/Checkbox",
component: Checkbox,
parameters: {
layout: "centered",
backgrounds: {
default: "dark",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#000000" },
],
},
},
argTypes: {
checked: {
control: "boolean",
description: "Whether the checkbox is checked",
},
mode: {
control: "select",
options: ["standard", "inverse"],
description: "Visual mode of the checkbox",
},
state: {
control: "select",
options: ["default", "hover", "focus"],
description: "Interaction state for static display",
},
disabled: {
control: "boolean",
description: "Whether the checkbox is disabled",
},
label: {
control: "text",
description: "Label text for the checkbox",
},
},
};
export const Default = {
args: {
checked: false,
mode: "standard",
state: "default",
disabled: false,
label: "Default checkbox",
},
play: DefaultInteraction.play,
render: (args) => {
const [checked, setChecked] = React.useState(args.checked);
return (
<Checkbox
{...args}
checked={checked}
onChange={({ checked: newChecked }) => setChecked(newChecked)}
/>
);
},
};
export const Checked = {
args: {
checked: true,
mode: "standard",
state: "default",
disabled: false,
label: "Checked checkbox",
},
play: CheckedInteraction.play,
render: (args) => {
const [checked, setChecked] = React.useState(args.checked);
return (
<Checkbox
{...args}
checked={checked}
onChange={({ checked: newChecked }) => setChecked(newChecked)}
/>
);
},
};
export const Standard = {
render: () => {
const [unchecked, setUnchecked] = React.useState(false);
const [checked, setChecked] = React.useState(true);
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Standard Mode</h3>
<div className="flex flex-col gap-2">
<Checkbox
label="Unchecked"
checked={unchecked}
mode="standard"
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
/>
<Checkbox
label="Checked"
checked={checked}
mode="standard"
onChange={({ checked: newChecked }) => setChecked(newChecked)}
/>
</div>
</div>
</div>
);
},
play: StandardInteraction.play,
};
export const Inverse = {
render: () => {
const [unchecked, setUnchecked] = React.useState(false);
const [checked, setChecked] = React.useState(true);
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Inverse Mode</h3>
<div className="flex flex-col gap-2">
<Checkbox
label="Unchecked"
checked={unchecked}
mode="inverse"
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
/>
<Checkbox
label="Checked"
checked={checked}
mode="inverse"
onChange={({ checked: newChecked }) => setChecked(newChecked)}
/>
</div>
</div>
</div>
);
},
play: InverseInteraction.play,
};
+138
View File
@@ -0,0 +1,138 @@
import React, { useState } from "react";
import ContextMenu from "../app/components/ContextMenu";
import ContextMenuItem from "../app/components/ContextMenuItem";
import ContextMenuSection from "../app/components/ContextMenuSection";
import ContextMenuDivider from "../app/components/ContextMenuDivider";
export default {
title: "Forms/ContextMenu",
component: ContextMenu,
argTypes: {
className: {
control: { type: "text" },
},
},
};
const Template = (args) => (
<ContextMenu {...args}>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
<ContextMenuDivider />
<ContextMenuItem selected>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuDivider />
<ContextMenuSection title="Section Title">
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
export const Default = Template.bind({});
export const WithCustomStyling = Template.bind({});
WithCustomStyling.args = {
className: "min-w-[250px]",
};
// Individual component stories
export const MenuItem = () => (
<div className="space-y-2">
<ContextMenuItem>Default Menu Item</ContextMenuItem>
<ContextMenuItem selected>Selected Menu Item</ContextMenuItem>
<ContextMenuItem hasSubmenu>Menu Item with Submenu</ContextMenuItem>
<ContextMenuItem disabled>Disabled Menu Item</ContextMenuItem>
</div>
);
export const MenuSection = () => (
<ContextMenu>
<ContextMenuSection title="First Section">
<ContextMenuItem>Item 1</ContextMenuItem>
<ContextMenuItem>Item 2</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Second Section">
<ContextMenuItem>Item 3</ContextMenuItem>
<ContextMenuItem>Item 4</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
export const MenuDivider = () => (
<ContextMenu>
<ContextMenuItem>Item Above</ContextMenuItem>
<ContextMenuDivider />
<ContextMenuItem>Item Below</ContextMenuItem>
</ContextMenu>
);
export const Interactive = () => {
const [selectedItem, setSelectedItem] = useState("");
return (
<ContextMenu>
<ContextMenuItem
selected={selectedItem === "item1"}
onClick={() => setSelectedItem("item1")}
>
Context Menu Item 1
</ContextMenuItem>
<ContextMenuItem
selected={selectedItem === "item2"}
onClick={() => setSelectedItem("item2")}
>
Context Menu Item 2
</ContextMenuItem>
<ContextMenuItem
selected={selectedItem === "item3"}
onClick={() => setSelectedItem("item3")}
>
Context Menu Item 3
</ContextMenuItem>
</ContextMenu>
);
};
// Comparison stories
export const AllVariants = () => (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium mb-2">Default Items</h3>
<ContextMenu>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
</ContextMenu>
</div>
<div>
<h3 className="text-sm font-medium mb-2">With Submenu Indicators</h3>
<ContextMenu>
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
</ContextMenu>
</div>
<div>
<h3 className="text-sm font-medium mb-2">With Selected Item</h3>
<ContextMenu>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem selected>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
</ContextMenu>
</div>
<div>
<h3 className="text-sm font-medium mb-2">With Sections</h3>
<ContextMenu>
<ContextMenuSection title="Section Title">
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
</div>
</div>
);
+269
View File
@@ -0,0 +1,269 @@
import React from "react";
import Input from "../app/components/Input";
export default {
title: "Forms/Input",
component: Input,
parameters: {
layout: "centered",
},
argTypes: {
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
labelVariant: {
control: { type: "select" },
options: ["default", "horizontal"],
},
state: {
control: { type: "select" },
options: ["default", "active", "hover", "focus", "error", "disabled"],
},
disabled: {
control: { type: "boolean" },
},
error: {
control: { type: "boolean" },
},
label: {
control: { type: "text" },
},
placeholder: {
control: { type: "text" },
},
value: {
control: { type: "text" },
},
},
};
const Template = (args) => <Input {...args} />;
// Default story
export const Default = Template.bind({});
Default.args = {
label: "Default Input",
placeholder: "Enter text...",
size: "medium",
labelVariant: "default",
state: "default",
};
// Size variants
export const Small = Template.bind({});
Small.args = {
label: "Small Input",
placeholder: "Small size",
size: "small",
labelVariant: "default",
state: "default",
};
export const Medium = Template.bind({});
Medium.args = {
label: "Medium Input",
placeholder: "Medium size",
size: "medium",
labelVariant: "default",
state: "default",
};
export const Large = Template.bind({});
Large.args = {
label: "Large Input",
placeholder: "Large size",
size: "large",
labelVariant: "default",
state: "default",
};
// Label variants
export const DefaultLabel = Template.bind({});
DefaultLabel.args = {
label: "Default Label (Top)",
placeholder: "Top label",
size: "medium",
labelVariant: "default",
state: "default",
};
export const HorizontalLabel = Template.bind({});
HorizontalLabel.args = {
label: "Horizontal Label",
placeholder: "Left label",
size: "medium",
labelVariant: "horizontal",
state: "default",
};
// States
export const Active = Template.bind({});
Active.args = {
label: "Active State",
placeholder: "Active input",
size: "medium",
labelVariant: "default",
state: "active",
};
export const Hover = Template.bind({});
Hover.args = {
label: "Hover State",
placeholder: "Hover input",
size: "medium",
labelVariant: "default",
state: "hover",
};
export const Focus = Template.bind({});
Focus.args = {
label: "Focus State",
placeholder: "Focused input",
size: "medium",
labelVariant: "default",
state: "focus",
};
export const Error = Template.bind({});
Error.args = {
label: "Error State",
placeholder: "Error input",
size: "medium",
labelVariant: "default",
state: "default",
error: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
label: "Disabled State",
placeholder: "Disabled input",
size: "medium",
labelVariant: "default",
state: "default",
disabled: true,
};
// Interactive example
export const Interactive = (args) => {
const [value, setValue] = React.useState("");
return (
<div className="space-y-4">
<Input
{...args}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p className="text-sm text-gray-600">Current value: "{value}"</p>
</div>
);
};
Interactive.args = {
label: "Interactive Input",
placeholder: "Type something...",
size: "medium",
labelVariant: "default",
state: "default",
};
// All sizes comparison
export const AllSizes = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Small Size</h3>
<div className="space-y-4">
<Input
label="Small Default"
placeholder="Small with top label"
size="small"
labelVariant="default"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Medium Size</h3>
<div className="space-y-4">
<Input
label="Medium Default"
placeholder="Medium with top label"
size="medium"
labelVariant="default"
/>
<Input
label="Medium Horizontal"
placeholder="Medium with left label"
size="medium"
labelVariant="horizontal"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Large Size</h3>
<div className="space-y-4">
<Input
label="Large Default"
placeholder="Large with top label"
size="large"
labelVariant="default"
/>
<Input
label="Large Horizontal"
placeholder="Large with left label"
size="large"
labelVariant="horizontal"
/>
</div>
</div>
</div>
);
// All states comparison
export const AllStates = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Input States</h3>
<div className="space-y-4">
<Input
label="Default State"
placeholder="Default input"
size="medium"
state="default"
/>
<Input
label="Active State"
placeholder="Active input"
size="medium"
state="active"
/>
<Input
label="Hover State"
placeholder="Hover input"
size="medium"
state="hover"
/>
<Input
label="Focus State"
placeholder="Focused input"
size="medium"
state="focus"
/>
<Input
label="Error State"
placeholder="Error input"
size="medium"
error={true}
/>
<Input
label="Disabled State"
placeholder="Disabled input"
size="medium"
disabled={true}
/>
</div>
</div>
</div>
);
+158
View File
@@ -0,0 +1,158 @@
import React from "react";
import RadioButton from "../app/components/RadioButton";
import {
DefaultInteraction,
CheckedInteraction,
StandardInteraction,
InverseInteraction,
KeyboardInteraction,
AccessibilityInteraction,
FormIntegration,
} from "../tests/storybook/RadioButton.interactions.test";
const meta = {
title: "Forms/RadioButton",
component: RadioButton,
parameters: {
layout: "centered",
backgrounds: {
default: "dark",
values: [{ name: "dark", value: "black" }],
},
},
tags: ["autodocs"],
argTypes: {
checked: { control: "boolean" },
mode: {
control: { type: "select" },
options: ["standard", "inverse"],
},
state: {
control: { type: "select" },
options: ["default", "hover", "focus"],
},
label: { control: "text" },
},
args: {
checked: false,
mode: "standard",
state: "default",
label: "Radio Button Label",
},
};
export default meta;
export const Default = {
args: {
checked: false,
mode: "standard",
state: "default",
label: "Default radio button",
},
play: DefaultInteraction.play,
render: (args) => {
const [checked, setChecked] = React.useState(args.checked);
return (
<RadioButton
{...args}
checked={checked}
onChange={({ checked: newChecked }) => setChecked(newChecked)}
/>
);
},
};
export const Checked = {
args: {
checked: true,
mode: "standard",
state: "default",
label: "Checked radio button",
},
play: CheckedInteraction.play,
render: (args) => {
const [checked, setChecked] = React.useState(args.checked);
return (
<RadioButton
{...args}
checked={checked}
onChange={({ checked: newChecked }) => setChecked(newChecked)}
/>
);
},
};
export const Standard = {
render: () => {
const [selectedValue, setSelectedValue] = React.useState("checked");
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Standard Mode</h3>
<div className="flex flex-col gap-2">
<RadioButton
label="Unchecked"
checked={selectedValue === "unchecked"}
name="standard-example"
value="unchecked"
mode="standard"
onChange={({ checked }) => {
if (checked) setSelectedValue("unchecked");
}}
/>
<RadioButton
label="Checked"
checked={selectedValue === "checked"}
name="standard-example"
value="checked"
mode="standard"
onChange={({ checked }) => {
if (checked) setSelectedValue("checked");
}}
/>
</div>
</div>
</div>
);
},
play: StandardInteraction.play,
};
export const Inverse = {
render: () => {
const [selectedValue, setSelectedValue] = React.useState("checked");
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Inverse Mode</h3>
<div className="flex flex-col gap-2">
<RadioButton
label="Unchecked"
checked={selectedValue === "unchecked"}
name="inverse-example"
value="unchecked"
mode="inverse"
onChange={({ checked }) => {
if (checked) setSelectedValue("unchecked");
}}
/>
<RadioButton
label="Checked"
checked={selectedValue === "checked"}
name="inverse-example"
value="checked"
mode="inverse"
onChange={({ checked }) => {
if (checked) setSelectedValue("checked");
}}
/>
</div>
</div>
</div>
);
},
play: InverseInteraction.play,
};
+151
View File
@@ -0,0 +1,151 @@
import React from "react";
import RadioGroup from "../app/components/RadioGroup";
import {
DefaultInteraction,
StandardInteraction,
InverseInteraction,
InteractiveInteraction,
KeyboardInteraction,
AccessibilityInteraction,
SingleSelectionInteraction,
FormIntegration,
} from "../tests/storybook/RadioGroup.interactions.test";
const meta = {
title: "Forms/RadioGroup",
component: RadioGroup,
parameters: {
layout: "centered",
backgrounds: {
default: "dark",
values: [{ name: "dark", value: "black" }],
},
},
tags: ["autodocs"],
argTypes: {
mode: {
control: { type: "select" },
options: ["standard", "inverse"],
},
state: {
control: { type: "select" },
options: ["default", "hover", "focus"],
},
value: { control: "text" },
},
args: {
mode: "standard",
state: "default",
value: "option1",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
},
};
export default meta;
export const Default = {
args: {
mode: "standard",
state: "default",
value: "option1",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
},
play: DefaultInteraction.play,
render: (args) => {
const [value, setValue] = React.useState(args.value);
return (
<RadioGroup
{...args}
value={value}
onChange={({ value: newValue }) => setValue(newValue)}
/>
);
},
};
export const Standard = {
render: () => {
const [value, setValue] = React.useState("option2");
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Standard Mode</h3>
<RadioGroup
name="standard-example"
value={value}
mode="standard"
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
onChange={({ value: newValue }) => setValue(newValue)}
/>
</div>
</div>
);
},
play: StandardInteraction.play,
};
export const Inverse = {
render: () => {
const [value, setValue] = React.useState("option1");
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Inverse Mode</h3>
<RadioGroup
name="inverse-example"
value={value}
mode="inverse"
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
onChange={({ value: newValue }) => setValue(newValue)}
/>
</div>
</div>
);
},
play: InverseInteraction.play,
};
export const Interactive = {
render: () => {
const [value, setValue] = React.useState("option1");
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-white font-medium">Interactive Example</h3>
<p className="text-gray-400 text-sm">Selected: {value}</p>
<RadioGroup
name="interactive-example"
value={value}
mode="standard"
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
onChange={({ value }) => setValue(value)}
/>
</div>
</div>
);
},
play: InteractiveInteraction.play,
};
+219
View File
@@ -0,0 +1,219 @@
import React, { useState } from "react";
import Select from "../app/components/Select";
export default {
title: "Forms/Select",
component: Select,
argTypes: {
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
labelVariant: {
control: { type: "select" },
options: ["default", "horizontal"],
},
state: {
control: { type: "select" },
options: ["default", "hover", "focus", "error", "disabled"],
},
disabled: {
control: { type: "boolean" },
},
error: {
control: { type: "boolean" },
},
placeholder: {
control: { type: "text" },
},
label: {
control: { type: "text" },
},
},
};
const Template = (args) => {
const [value, setValue] = useState("");
return (
<Select
{...args}
value={value}
onChange={setValue}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
);
};
export const Default = Template.bind({});
Default.args = {
label: "Default Select",
placeholder: "Select",
};
export const Small = Template.bind({});
Small.args = {
label: "Small Select",
size: "small",
placeholder: "Select",
};
export const Medium = Template.bind({});
Medium.args = {
label: "Medium Select",
size: "medium",
placeholder: "Select",
};
export const Large = Template.bind({});
Large.args = {
label: "Large Select",
size: "large",
placeholder: "Select",
};
export const DefaultLabel = Template.bind({});
DefaultLabel.args = {
label: "Default (Top Label)",
labelVariant: "default",
placeholder: "Select",
};
export const HorizontalLabel = Template.bind({});
HorizontalLabel.args = {
label: "Horizontal (Left Label)",
labelVariant: "horizontal",
placeholder: "Select",
};
export const Active = Template.bind({});
Active.args = {
label: "Active State",
state: "default",
placeholder: "Select",
};
export const Hover = Template.bind({});
Hover.args = {
label: "Hover State",
state: "hover",
placeholder: "Select",
};
export const Focus = Template.bind({});
Focus.args = {
label: "Focus State",
state: "focus",
placeholder: "Select",
};
export const Error = Template.bind({});
Error.args = {
label: "Error State",
error: true,
placeholder: "Select",
};
export const Disabled = Template.bind({});
Disabled.args = {
label: "Disabled State",
disabled: true,
placeholder: "Select",
};
export const Interactive = Template.bind({});
Interactive.args = {
label: "Interactive Select",
placeholder: "Choose an option",
};
// Comparison stories
export const AllSizes = () => {
const [smallValue, setSmallValue] = useState("");
const [mediumValue, setMediumValue] = useState("");
const [largeValue, setLargeValue] = useState("");
return (
<div className="space-y-4">
<Select
label="Small"
size="small"
value={smallValue}
onChange={(e) => setSmallValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
label="Medium"
size="medium"
value={mediumValue}
onChange={(e) => setMediumValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
label="Large"
size="large"
value={largeValue}
onChange={(e) => setLargeValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
</div>
);
};
export const AllStates = () => {
const [defaultValue, setDefaultValue] = useState("");
const [errorValue, setErrorValue] = useState("");
const [disabledValue, setDisabledValue] = useState("");
return (
<div className="space-y-4">
<Select
label="Default State"
value={defaultValue}
onChange={(e) => setDefaultValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
label="Error State"
error={true}
value={errorValue}
onChange={(e) => setErrorValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
label="Disabled State"
disabled={true}
value={disabledValue}
onChange={(e) => setDisabledValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
</div>
);
};
+128
View File
@@ -0,0 +1,128 @@
import React from "react";
import Switch from "../app/components/Switch";
export default {
title: "Forms/Switch",
component: Switch,
parameters: {
layout: "centered",
},
argTypes: {
checked: {
control: "boolean",
description: "Whether the switch is checked (on) or not (off)",
},
state: {
control: "select",
options: ["default", "focus"],
description: "Visual state of the switch",
},
label: {
control: "text",
description: "Label text displayed next to the switch",
},
onChange: {
action: "changed",
description: "Callback fired when the switch is toggled",
},
onFocus: {
action: "focused",
description: "Callback fired when the switch receives focus",
},
onBlur: {
action: "blurred",
description: "Callback fired when the switch loses focus",
},
},
};
const Template = (args) => <Switch {...args} />;
export const Default = Template.bind({});
Default.args = {
checked: false,
label: "Switch label",
};
export const Checked = Template.bind({});
Checked.args = {
checked: true,
label: "Switch label",
};
export const Focus = Template.bind({});
Focus.args = {
checked: false,
state: "focus",
label: "Switch label",
};
export const FocusChecked = Template.bind({});
FocusChecked.args = {
checked: true,
state: "focus",
label: "Switch label",
};
export const States = () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Switch States</h3>
<div className="space-y-4">
<Switch checked={false} label="Switch label" />
<Switch checked={true} label="Switch label" />
<Switch checked={false} state="focus" label="Switch label" />
<Switch checked={true} state="focus" label="Switch label" />
</div>
</div>
</div>
);
export const Interactive = () => {
const [checked, setChecked] = React.useState(false);
const [state, setState] = React.useState("default");
return (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Interactive Switch</h3>
<Switch
checked={checked}
state={state}
onChange={() => setChecked(!checked)}
label="Enable notifications"
/>
</div>
<div className="space-y-4">
<h4 className="text-md font-semibold">Controls</h4>
<div className="space-y-2">
<div>
<label className="block text-sm font-medium mb-1">State:</label>
<select
value={state}
onChange={(e) => setState(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded"
>
<option value="default">Default</option>
<option value="focus">Focus</option>
</select>
</div>
</div>
</div>
</div>
);
};
export const WithText = () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Switch with Different Labels</h3>
<div className="space-y-4">
<Switch checked={false} label="Enable notifications" />
<Switch checked={true} label="Auto-save documents" />
<Switch checked={false} label="Dark mode" />
<Switch checked={true} label="Email updates" />
</div>
</div>
</div>
);
+286
View File
@@ -0,0 +1,286 @@
import React from "react";
import TextArea from "../app/components/TextArea";
export default {
title: "Forms/TextArea",
component: TextArea,
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: {
control: { type: "boolean" },
},
error: {
control: { type: "boolean" },
},
},
};
const Template = (args) => <TextArea {...args} />;
export const Default = Template.bind({});
Default.args = {
label: "Text Area",
placeholder: "Enter text...",
value: "",
};
export const WithValue = Template.bind({});
WithValue.args = {
label: "Text Area",
placeholder: "Enter text...",
value:
"This is some sample text content that demonstrates how the text area looks with content.",
};
export const Small = Template.bind({});
Small.args = {
size: "small",
label: "Small Text Area",
placeholder: "Enter text...",
value: "",
};
export const Medium = Template.bind({});
Medium.args = {
size: "medium",
label: "Medium Text Area",
placeholder: "Enter text...",
value: "",
};
export const Large = Template.bind({});
Large.args = {
size: "large",
label: "Large Text Area",
placeholder: "Enter text...",
value: "",
};
export const HorizontalLabel = Template.bind({});
HorizontalLabel.args = {
labelVariant: "horizontal",
label: "Horizontal Label",
placeholder: "Enter text...",
value: "",
};
export const AllSizes = () => (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Default Label Variant</h3>
<div className="space-y-4">
<TextArea
size="small"
label="Small Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="medium"
label="Medium Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="large"
label="Large Text Area"
placeholder="Enter text..."
value=""
/>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Horizontal Label Variant</h3>
<div className="space-y-4">
<TextArea
size="small"
labelVariant="horizontal"
label="Small Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="large"
labelVariant="horizontal"
label="Large Text Area"
placeholder="Enter text..."
value=""
/>
</div>
</div>
</div>
);
export const States = () => (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Default Label Variant</h3>
<div className="space-y-4">
<TextArea label="Default State" placeholder="Enter text..." value="" />
<TextArea
label="Active State"
placeholder="Enter text..."
value=""
state="active"
/>
<TextArea
label="Hover State"
placeholder="Enter text..."
value=""
state="hover"
/>
<TextArea
label="Focus State"
placeholder="Enter text..."
value=""
state="focus"
/>
<TextArea
label="Error State"
placeholder="Enter text..."
value=""
error
/>
<TextArea
label="Disabled State"
placeholder="Enter text..."
value=""
disabled
/>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Horizontal Label Variant</h3>
<div className="space-y-4">
<TextArea
labelVariant="horizontal"
label="Default State"
placeholder="Enter text..."
value=""
/>
<TextArea
labelVariant="horizontal"
label="Active State"
placeholder="Enter text..."
value=""
state="active"
/>
<TextArea
labelVariant="horizontal"
label="Hover State"
placeholder="Enter text..."
value=""
state="hover"
/>
<TextArea
labelVariant="horizontal"
label="Focus State"
placeholder="Enter text..."
value=""
state="focus"
/>
<TextArea
labelVariant="horizontal"
label="Error State"
placeholder="Enter text..."
value=""
error
/>
<TextArea
labelVariant="horizontal"
label="Disabled State"
placeholder="Enter text..."
value=""
disabled
/>
</div>
</div>
</div>
);
export const Interactive = () => {
const [value, setValue] = React.useState("");
const [state, setState] = React.useState("default");
const [disabled, setDisabled] = React.useState(false);
const [error, setError] = React.useState(false);
return (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Interactive TextArea</h3>
<div className="space-y-4">
<TextArea
label="Interactive Text Area"
placeholder="Type something..."
value={value}
onChange={(e) => setValue(e.target.value)}
state={state}
disabled={disabled}
error={error}
/>
</div>
</div>
<div className="space-y-4">
<h4 className="text-md font-semibold">Controls</h4>
<div className="space-y-2">
<div>
<label className="block text-sm font-medium mb-1">State:</label>
<select
value={state}
onChange={(e) => setState(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded"
>
<option value="default">Default</option>
<option value="active">Active</option>
<option value="hover">Hover</option>
<option value="focus">Focus</option>
</select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="disabled"
checked={disabled}
onChange={(e) => setDisabled(e.target.checked)}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="error"
checked={error}
onChange={(e) => setError(e.target.checked)}
/>
<label htmlFor="error" className="text-sm">
Error
</label>
</div>
</div>
</div>
</div>
);
};
+122
View File
@@ -0,0 +1,122 @@
import React from "react";
import Toggle from "../app/components/Toggle";
export default {
title: "Forms/Toggle",
component: Toggle,
parameters: {
layout: "centered",
},
argTypes: {
state: {
control: { type: "select" },
options: ["default", "hover", "focus"],
},
disabled: {
control: { type: "boolean" },
},
checked: {
control: { type: "boolean" },
},
showIcon: {
control: { type: "boolean" },
},
showText: {
control: { type: "boolean" },
},
},
};
const Template = (args) => <Toggle {...args} />;
export const States = () => (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Toggle States</h3>
<div className="space-y-4">
<Toggle label="Default State" checked={false} />
<Toggle label="Hover State" checked={false} state="hover" />
<Toggle label="Selected State" checked={true} />
<Toggle label="Focus State" checked={false} state="focus" />
<Toggle label="Disabled State" checked={false} disabled />
</div>
</div>
</div>
);
export const WithText = Template.bind({});
WithText.args = {
label: "Text Toggle",
checked: false,
showText: true,
text: "Toggle",
};
export const WithIcon = Template.bind({});
WithIcon.args = {
label: "Icon Toggle",
checked: false,
showIcon: true,
icon: "I",
};
export const Interactive = () => {
const [checked, setChecked] = React.useState(false);
const [state, setState] = React.useState("default");
const [disabled, setDisabled] = React.useState(false);
return (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Interactive Toggle</h3>
<div className="space-y-4">
<Toggle
label="Interactive Toggle"
checked={checked}
onChange={() => setChecked(!checked)}
state={state}
disabled={disabled}
/>
</div>
</div>
<div className="space-y-4">
<h4 className="text-md font-semibold">Controls</h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="checked"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
<label htmlFor="checked" className="text-sm">
Checked
</label>
</div>
<div>
<label className="block text-sm font-medium mb-1">State:</label>
<select
value={state}
onChange={(e) => setState(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded"
>
<option value="default">Default</option>
<option value="focus">Focus</option>
</select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="disabled"
checked={disabled}
onChange={(e) => setDisabled(e.target.checked)}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
</div>
</div>
</div>
);
};
+210
View File
@@ -0,0 +1,210 @@
import React from "react";
import ToggleGroup from "../app/components/ToggleGroup";
export default {
title: "Forms/ToggleGroup",
component: ToggleGroup,
parameters: {
layout: "centered",
},
argTypes: {
position: {
control: { type: "select" },
options: ["left", "middle", "right"],
},
state: {
control: { type: "select" },
options: ["default", "hover", "focus", "selected"],
},
showText: {
control: { type: "boolean" },
},
},
};
const Template = (args) => <ToggleGroup {...args}>Toggle Item</ToggleGroup>;
export const Default = Template.bind({});
Default.args = {
position: "left",
state: "default",
showText: true,
};
export const Middle = Template.bind({});
Middle.args = {
position: "middle",
state: "default",
showText: true,
};
export const Right = Template.bind({});
Right.args = {
position: "right",
state: "default",
showText: true,
};
export const States = () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Toggle Group States</h3>
<div className="flex space-x-4">
<ToggleGroup position="left" state="default">
Default
</ToggleGroup>
<ToggleGroup position="middle" state="hover">
Hover
</ToggleGroup>
<ToggleGroup position="middle" state="focus">
Focus
</ToggleGroup>
<ToggleGroup position="right" state="selected">
Selected
</ToggleGroup>
</div>
</div>
</div>
);
export const Positions = () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Toggle Group Positions</h3>
<div className="flex">
<ToggleGroup position="left" state="default">
Left
</ToggleGroup>
<ToggleGroup position="middle" state="default">
Middle
</ToggleGroup>
<ToggleGroup position="middle" state="default">
Middle
</ToggleGroup>
<ToggleGroup position="right" state="default">
Right
</ToggleGroup>
</div>
</div>
</div>
);
export const WithText = Template.bind({});
WithText.args = {
position: "left",
state: "default",
showText: true,
children: "Active Deals",
};
export const WithoutText = Template.bind({});
WithoutText.args = {
position: "left",
state: "default",
showText: false,
children: "☰",
};
export const WithIcons = () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Toggle Group with Icons</h3>
<div className="flex">
<ToggleGroup
position="left"
state="default"
showText={false}
ariaLabel="Menu"
>
</ToggleGroup>
<ToggleGroup
position="middle"
state="selected"
showText={false}
ariaLabel="Menu"
>
</ToggleGroup>
<ToggleGroup
position="right"
state="default"
showText={false}
ariaLabel="Menu"
>
</ToggleGroup>
</div>
</div>
</div>
);
export const Interactive = () => {
const [selectedPosition, setSelectedPosition] = React.useState("left");
const [state, setState] = React.useState("default");
const [showText, setShowText] = React.useState(true);
return (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Interactive Toggle Group</h3>
<div className="flex">
<ToggleGroup
position="left"
state={selectedPosition === "left" ? "selected" : state}
showText={showText}
onChange={() => setSelectedPosition("left")}
ariaLabel={!showText ? "Active Deals" : undefined}
>
{showText ? "Active Deals" : "☰"}
</ToggleGroup>
<ToggleGroup
position="middle"
state={selectedPosition === "middle" ? "selected" : state}
showText={showText}
onChange={() => setSelectedPosition("middle")}
ariaLabel={!showText ? "Inactive Deals" : undefined}
>
{showText ? "Inactive Deals" : "☰"}
</ToggleGroup>
<ToggleGroup
position="right"
state={selectedPosition === "right" ? "selected" : state}
showText={showText}
onChange={() => setSelectedPosition("right")}
ariaLabel={!showText ? "Pending Deals" : undefined}
>
{showText ? "Pending Deals" : "☰"}
</ToggleGroup>
</div>
</div>
<div className="space-y-4">
<h4 className="text-md font-semibold">Controls</h4>
<div className="space-y-2">
<div>
<label className="block text-sm font-medium mb-1">State:</label>
<select
value={state}
onChange={(e) => setState(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded"
>
<option value="default">Default</option>
<option value="focus">Focus</option>
</select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showText"
checked={showText}
onChange={(e) => setShowText(e.target.checked)}
/>
<label htmlFor="showText" className="text-sm">
Show Text
</label>
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,399 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
expect.extend(toHaveNoViolations);
describe("ContextMenu Components Accessibility", () => {
describe("ContextMenu Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role and structure", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const menu = screen.getByRole("menu");
expect(menu).toBeInTheDocument();
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(2);
});
it("has proper focus management", async () => {
const user = userEvent.setup();
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const firstItem = screen.getByRole("menuitem", { name: "Item 1" });
expect(firstItem).toHaveAttribute("tabIndex", "0");
expect(firstItem).toBeInTheDocument();
});
});
describe("ContextMenuItem Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper ARIA attributes", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).not.toHaveAttribute("aria-current");
});
it("updates aria-current when selected", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Test Item
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveAttribute("aria-current", "true");
});
it("is keyboard accessible", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<ContextMenu>
<ContextMenuItem onClick={onClick}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
item.focus();
await user.keyboard("{Enter}");
expect(onClick).toHaveBeenCalled();
});
it("is accessible with Space key", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<ContextMenu>
<ContextMenuItem onClick={onClick}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
item.focus();
await user.keyboard(" ");
expect(onClick).toHaveBeenCalled();
});
it("has proper focus indicators", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
it("announces selection state to screen readers", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Test Item
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveAttribute("aria-current", "true");
});
});
describe("ContextMenuSection Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper heading structure", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const title = screen.getByText("Test Section");
expect(title).toBeInTheDocument();
});
it("has sufficient color contrast for section title", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const title = screen.getByText("Test Section");
expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
});
});
describe("ContextMenuDivider Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuDivider />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper semantic structure", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toBeInTheDocument();
});
it("has sufficient visual contrast", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
});
describe("Integrated Menu Accessibility", () => {
const TestMenu = () => (
<ContextMenu>
<ContextMenuSection title="First Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Second Section">
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Item 3
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
it("has no accessibility violations when integrated", async () => {
const { container } = render(<TestMenu />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper menu structure", () => {
render(<TestMenu />);
const menu = screen.getByRole("menu");
expect(menu).toBeInTheDocument();
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(3);
expect(screen.getByText("First Section")).toBeInTheDocument();
expect(screen.getByText("Second Section")).toBeInTheDocument();
});
it("maintains proper focus order", async () => {
const user = userEvent.setup();
render(<TestMenu />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(3);
// Check that all items are focusable
items.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
});
it("handles keyboard navigation correctly", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<ContextMenu>
<ContextMenuItem onClick={onClick}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard("{Enter}");
expect(onClick).toHaveBeenCalled();
});
});
describe("Color Contrast", () => {
it("has sufficient contrast for menu items", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"text-[var(--color-content-default-brand-primary)]",
);
});
it("has sufficient contrast for section titles", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section" />
</ContextMenu>,
);
const title = screen.getByText("Test Section");
expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
});
it("has sufficient contrast for dividers", () => {
render(
<ContextMenu>
<ContextMenuDivider />
</ContextMenu>,
);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
});
describe("Screen Reader Support", () => {
it("announces menu structure correctly", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const menu = screen.getByRole("menu");
expect(menu).toBeInTheDocument();
const items = screen.getAllByRole("menuitem");
expect(items[0]).not.toHaveAttribute("aria-current");
expect(items[1]).toHaveAttribute("aria-current", "true");
});
it("announces selection state changes", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ContextMenuItem onClick={vi.fn()} selected={false}>
Test Item
</ContextMenuItem>,
);
const item = screen.getByRole("menuitem");
expect(item).not.toHaveAttribute("aria-current");
rerender(
<ContextMenuItem onClick={vi.fn()} selected={true}>
Test Item
</ContextMenuItem>,
);
expect(item).toHaveAttribute("aria-current", "true");
});
});
describe("WCAG Compliance", () => {
it("meets WCAG 2.1 AA standards", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuItem onClick={vi.fn()}>Item 3</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards in all states", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Selected Item
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Submenu Item
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} disabled={true}>
Disabled Item
</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
+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();
});
});
+307
View File
@@ -0,0 +1,307 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Select from "../../app/components/Select";
expect.extend(toHaveNoViolations);
describe("Select Component Accessibility", () => {
const defaultProps = {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
};
describe("ARIA Attributes", () => {
it("has correct initial ARIA attributes", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAttribute("aria-expanded", "false");
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
});
it("updates aria-expanded when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveAttribute("aria-expanded", "true");
});
});
it("has proper role for dropdown menu", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
const listbox = screen.getByRole("listbox");
expect(listbox).toBeInTheDocument();
});
});
it("has proper role for menu items", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
const options = screen.getAllByRole("option");
expect(options).toHaveLength(3);
expect(options[0]).toHaveTextContent("Option 1");
expect(options[1]).toHaveTextContent("Option 2");
expect(options[2]).toHaveTextContent("Option 3");
});
});
});
describe("Keyboard Navigation", () => {
it("opens dropdown with Enter key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
});
it("opens dropdown with Space key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard(" ");
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
});
it("closes dropdown with Escape key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
await user.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
});
it("selects option with click", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<Select {...defaultProps} onChange={onChange} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(onChange).toHaveBeenCalledWith({
target: { value: "option1", text: "Option 1" },
});
});
});
describe("Screen Reader Support", () => {
it("announces selected option", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} value="option2" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveTextContent("Option 2");
});
it("announces placeholder when no option selected", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveTextContent("Select an option");
});
it("has accessible name from label", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAccessibleName("Test Select");
});
});
describe("Focus Management", () => {
it("maintains focus on select button when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveFocus();
});
});
it("returns focus to select button after selection", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
await waitFor(() => {
expect(selectButton).toHaveFocus();
});
});
});
describe("Disabled State", () => {
it("is not focusable when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toBeDisabled();
await user.tab();
expect(selectButton).not.toHaveFocus();
});
it("has correct ARIA attributes when disabled", () => {
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toBeDisabled();
});
});
describe("Error State", () => {
it("announces error state to screen readers", () => {
render(<Select {...defaultProps} error={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
});
describe("WCAG Compliance", () => {
it("meets WCAG 2.1 AA standards", async () => {
const { container } = render(<Select {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards in disabled state", async () => {
const { container } = render(
<Select {...defaultProps} disabled={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards in error state", async () => {
const { container } = render(<Select {...defaultProps} error={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards when dropdown is open", async () => {
const user = userEvent.setup();
const { container } = render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
describe("Color Contrast", () => {
it("has sufficient color contrast for text", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"text-[var(--color-content-default-primary)]",
);
});
it("has sufficient color contrast for labels", () => {
render(<Select {...defaultProps} />);
const label = screen.getByText("Test Select");
expect(label).toHaveClass(
"text-[var(--color-content-default-secondary)]",
);
});
});
describe("Focus Indicators", () => {
it("has visible focus indicator", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass(
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
);
});
it("distinguishes between focus and hover states", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
// Focus state should be different from hover state
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass(
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
});
});
+98
View File
@@ -0,0 +1,98 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Switch from "../../app/components/Switch";
expect.extend(toHaveNoViolations);
describe("Switch Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<Switch checked={false} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("role", "switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
});
it("has proper ARIA attributes when checked", () => {
render(<Switch checked={true} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("has proper ARIA attributes when focused", () => {
render(<Switch state="focus" label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
expect(switchButton).toHaveClass("rounded-full");
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
});
it("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("handles focus state accessibility", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("handles checked state accessibility", () => {
const { rerender } = render(<Switch checked={false} label="Test Switch" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} label="Test Switch" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("has no accessibility violations", async () => {
const { container } = render(<Switch label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when checked", async () => {
const { container } = render(<Switch checked={true} label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when focused", async () => {
const { container } = render(<Switch state="focus" label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations with text", async () => {
const { container } = render(<Switch label="Enable notifications" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations without text", async () => {
const { container } = render(<Switch />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
+121
View File
@@ -0,0 +1,121 @@
import { expect, test, describe, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe, toHaveNoViolations } from "jest-axe";
import TextArea from "../../app/components/TextArea";
expect.extend(toHaveNoViolations);
describe("TextArea Accessibility", () => {
test("renders without accessibility violations", async () => {
const { container } = render(<TextArea label="Test TextArea" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has proper label association", () => {
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Test Label");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
});
test("has proper ARIA attributes", () => {
render(<TextArea label="Test Label" name="test-textarea" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveAttribute("id");
expect(textarea).toHaveAttribute("name", "test-textarea");
});
test("supports keyboard navigation", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.tab();
expect(textarea).toHaveFocus();
});
test("announces changes to screen readers", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea label="Test Label" onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(textarea).toHaveValue("test");
});
test("handles disabled state accessibility", () => {
render(<TextArea label="Test Label" disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
expect(textarea).toHaveAttribute("aria-disabled", "true");
});
test("handles error state accessibility", () => {
render(<TextArea label="Test Label" error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveAttribute("aria-invalid", "true");
});
test("maintains focus management", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(textarea).toHaveFocus();
});
test("supports horizontal label layout", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Horizontal Label");
expect(textarea).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
test("handles different sizes accessibility", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("maintains proper contrast ratios", () => {
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Test Label");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("supports screen reader announcements for state changes", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.type(textarea, "Hello");
expect(textarea).toHaveValue("Hello");
});
});
+112
View File
@@ -0,0 +1,112 @@
import { expect, test, describe, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import Toggle from "../../app/components/Toggle";
expect.extend(toHaveNoViolations);
describe("Toggle Accessibility", () => {
test("has proper ARIA attributes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
expect(toggle).toHaveAttribute("type", "button");
});
test("has proper ARIA attributes when checked", () => {
render(<Toggle label="Test Toggle" checked={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
});
test("has proper ARIA attributes when disabled", () => {
render(<Toggle label="Test Toggle" disabled={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
});
test("has proper label association", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
const label = screen.getByText("Test Toggle");
expect(toggle).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
test("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
toggle.focus();
expect(toggle).toHaveFocus();
fireEvent.keyDown(toggle, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
fireEvent.keyDown(toggle, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
test("handles disabled state accessibility", () => {
const handleChange = vi.fn();
render(
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
expect(toggle).toHaveClass("cursor-not-allowed");
fireEvent.click(toggle);
expect(handleChange).not.toHaveBeenCalled();
});
test("handles focus state accessibility", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
});
test("has no accessibility violations", async () => {
const { container } = render(<Toggle label="Test Toggle" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when checked", async () => {
const { container } = render(<Toggle label="Test Toggle" checked={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when disabled", async () => {
const { container } = render(
<Toggle label="Test Toggle" disabled={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations with icon", async () => {
const { container } = render(
<Toggle label="Test Toggle" showIcon={true} icon="I" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations with text", async () => {
const { container } = render(
<Toggle label="Test Toggle" showText={true} text="Toggle" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
@@ -0,0 +1,92 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ToggleGroup from "../../app/components/ToggleGroup";
expect.extend(toHaveNoViolations);
describe("ToggleGroup Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<ToggleGroup>Toggle Item</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveAttribute("type", "button");
expect(toggleGroup).toHaveAttribute("role", "button");
});
it("has proper ARIA attributes when focused", () => {
render(<ToggleGroup state="focus">Focused Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveAttribute("type", "button");
expect(toggleGroup).toHaveAttribute("role", "button");
});
it("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Keyboard Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
// Test Enter key
fireEvent.keyDown(toggleGroup, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(toggleGroup, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("handles focus state accessibility", () => {
render(<ToggleGroup state="focus">Focus Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
it("handles selected state accessibility", () => {
render(<ToggleGroup state="selected">Selected Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
expect(toggleGroup).toHaveClass(
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("has no accessibility violations", async () => {
const { container } = render(<ToggleGroup>Accessible Toggle</ToggleGroup>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when focused", async () => {
const { container } = render(
<ToggleGroup state="focus">Focused Toggle</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when selected", async () => {
const { container } = render(
<ToggleGroup state="selected">Selected Toggle</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations with text", async () => {
const { container } = render(
<ToggleGroup showText={true}>Text Toggle</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations without text", async () => {
const { container } = render(
<ToggleGroup showText={false} ariaLabel="Icon Toggle">
Icon Toggle
</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
@@ -0,0 +1,158 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { expect, test, describe } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Checkbox from "../../../app/components/Checkbox";
expect.extend(toHaveNoViolations);
describe("Checkbox Accessibility", () => {
test("should not have accessibility violations when unchecked", async () => {
const { container } = render(<Checkbox label="Test checkbox" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should not have accessibility violations when checked", async () => {
const { container } = render(
<Checkbox label="Test checkbox" checked={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should not have accessibility violations when disabled", async () => {
const { container } = render(
<Checkbox label="Test checkbox" disabled={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should not have accessibility violations in inverse mode", async () => {
const { container } = render(
<Checkbox label="Test checkbox" mode="inverse" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should have proper ARIA attributes", () => {
render(<Checkbox label="Test checkbox" checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should have proper ARIA attributes when disabled", () => {
render(<Checkbox label="Test checkbox" disabled={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
expect(checkbox).toHaveAttribute("aria-disabled", "true");
expect(checkbox).toHaveAttribute("tabIndex", "-1");
});
test("should have proper ARIA attributes when checked", () => {
render(<Checkbox label="Test checkbox" checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should have proper ARIA attributes when unchecked", () => {
render(<Checkbox label="Test checkbox" checked={false} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should have proper ARIA attributes with custom aria-label", () => {
render(<Checkbox ariaLabel="Custom accessibility label" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute(
"aria-label",
"Custom accessibility label",
);
});
test("should have proper focus management", () => {
const { rerender } = render(<Checkbox label="Test checkbox" />);
const checkbox = screen.getByRole("checkbox");
// Should be focusable when not disabled
expect(checkbox).toHaveAttribute("tabIndex", "0");
// Should not be focusable when disabled
rerender(<Checkbox label="Test checkbox disabled" disabled={true} />);
const disabledCheckbox = screen.getByRole("checkbox");
expect(disabledCheckbox).toHaveAttribute("tabIndex", "-1");
});
test("should have proper keyboard navigation", () => {
render(<Checkbox label="Test checkbox" />);
const checkbox = screen.getByRole("checkbox");
// Should be focusable
expect(checkbox).toHaveAttribute("tabIndex", "0");
// Should support keyboard interaction
expect(checkbox).toHaveAttribute("role", "checkbox");
});
test("should have proper semantic structure", () => {
render(<Checkbox label="Test checkbox" />);
// Should have a label element
const label = screen.getByText("Test checkbox").closest("label");
expect(label).toBeInTheDocument();
// Should have a checkbox role
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
// Should be associated with the label
expect(label).toContainElement(checkbox);
});
test("should have proper color contrast", async () => {
const { container } = render(<Checkbox label="Test checkbox" />);
const results = await axe(container);
// Check for color contrast violations
const contrastViolations = results.violations.filter(
(violation) => violation.id === "color-contrast",
);
expect(contrastViolations).toHaveLength(0);
});
test("should have proper focus indicators", async () => {
const { container } = render(<Checkbox label="Test checkbox" />);
const results = await axe(container);
// Check for focus indicator violations
const focusViolations = results.violations.filter(
(violation) => violation.id === "focus-order-semantics",
);
expect(focusViolations).toHaveLength(0);
});
test("should have proper form integration", () => {
render(<Checkbox name="test-checkbox" value="test-value" checked={true} />);
// Should have hidden input for form submission
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "checkbox");
expect(hiddenInput).toHaveAttribute("name", "test-checkbox");
expect(hiddenInput).toBeChecked();
});
});
@@ -0,0 +1,235 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../../app/components/RadioButton";
describe("RadioButton Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<RadioButton label="Test Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("role", "radio");
expect(radioButton).toHaveAttribute("aria-checked", "false");
expect(radioButton).toHaveAttribute("tabIndex", "0");
});
it("updates aria-checked when checked state changes", () => {
const { rerender } = render(
<RadioButton checked={false} label="Test Radio" />,
);
let radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "false");
rerender(<RadioButton checked={true} label="Test Radio" />);
radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("associates label with radio button", () => {
render(<RadioButton label="Accessible Radio" />);
const radioButton = screen.getByRole("radio");
const labelId = radioButton.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent("Accessible Radio");
});
it("uses aria-label when provided", () => {
render(<RadioButton ariaLabel="Custom Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
expect(radioButton).not.toHaveAttribute("aria-labelledby");
});
it("prioritizes aria-label over aria-labelledby", () => {
render(<RadioButton label="Visible Label" ariaLabel="Hidden Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Hidden Aria Label");
expect(radioButton).not.toHaveAttribute("aria-labelledby");
});
it("is keyboard accessible", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Keyboard Radio" />);
const radioButton = screen.getByRole("radio");
radioButton.focus();
expect(radioButton).toHaveFocus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalled();
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Enter Radio" />);
const radioButton = screen.getByRole("radio");
await user.click(radioButton); // Focus the element first
await user.keyboard("Enter");
expect(handleChange).toHaveBeenCalled();
});
it("handles Space key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Space Radio" />);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalled();
});
it("ignores other keys", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Other Keys Radio" />);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard("a");
await user.keyboard("Tab");
await user.keyboard("Escape");
expect(handleChange).not.toHaveBeenCalled();
});
it("has proper tab order", () => {
render(
<div>
<RadioButton label="First Radio" />
<RadioButton label="Second Radio" />
<RadioButton label="Third Radio" />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
});
});
it("generates unique IDs for accessibility", () => {
render(
<div>
<RadioButton label="Radio 1" />
<RadioButton label="Radio 2" />
<RadioButton label="Radio 3" />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
const ids = radioButtons.map((button) => button.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(3);
expect(ids.every((id) => id.startsWith("radio-"))).toBe(true);
});
it("uses provided ID for accessibility", () => {
render(<RadioButton id="custom-radio-id" label="Custom ID Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("id", "custom-radio-id");
});
it("has accessible name from label", () => {
render(<RadioButton label="Accessible Name Radio" />);
const radioButton = screen.getByRole("radio");
const accessibleName = radioButton.getAttribute("aria-labelledby");
const labelElement = document.getElementById(accessibleName);
expect(labelElement).toHaveTextContent("Accessible Name Radio");
});
it("has accessible name from aria-label", () => {
render(<RadioButton ariaLabel="Aria Label Name" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Aria Label Name");
});
it("maintains focus management", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
const { rerender } = render(
<RadioButton
checked={false}
onChange={handleChange}
label="Focus Radio"
/>,
);
const radioButton = screen.getByRole("radio");
radioButton.focus();
expect(radioButton).toHaveFocus();
// Change checked state
rerender(
<RadioButton
checked={true}
onChange={handleChange}
label="Focus Radio"
/>,
);
// Should still be focusable
expect(radioButton).toHaveAttribute("tabIndex", "0");
});
it("has proper role and state", () => {
render(<RadioButton checked={true} label="State Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("role", "radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("supports screen reader navigation", () => {
render(
<div>
<RadioButton label="First Option" />
<RadioButton label="Second Option" />
<RadioButton label="Third Option" />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
// All should be in tab order
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
expect(button).toHaveAttribute("role", "radio");
});
});
it("has proper form association", () => {
render(
<RadioButton name="test-radio" value="test-value" label="Form Radio" />,
);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toHaveAttribute("type", "radio");
expect(hiddenInput).toHaveAttribute("name", "test-radio");
expect(hiddenInput).toHaveAttribute("value", "test-value");
});
});
@@ -0,0 +1,317 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../../app/components/RadioGroup";
describe("RadioGroup Accessibility", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("has proper radiogroup role", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
});
it("has proper ARIA attributes on radiogroup", () => {
render(
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
});
it("has proper radio button roles", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons).toHaveLength(3);
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
});
});
it("shows correct selection state", () => {
render(<RadioGroup options={defaultOptions} value="option2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
});
it("updates selection state correctly", () => {
const { rerender } = render(
<RadioGroup options={defaultOptions} value="option1" />,
);
let radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
rerender(<RadioGroup options={defaultOptions} value="option3" />);
radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
});
it("associates labels with radio buttons", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button, index) => {
const labelId = button.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent(`Option ${index + 1}`);
});
});
it("uses aria-label when provided in options", () => {
const optionsWithAria = [
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
];
render(<RadioGroup options={optionsWithAria} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
});
it("is keyboard accessible", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second option
radioButtons[1].focus();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
await user.click(radioButtons[2]); // Focus the element first
await user.keyboard("Enter");
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("handles Space key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("ignores other keys", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard("a");
await user.keyboard("Tab");
await user.keyboard("Escape");
expect(handleChange).not.toHaveBeenCalled();
});
it("has proper tab order", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
});
});
it("generates unique IDs for accessibility", () => {
render(
<div>
<RadioGroup options={defaultOptions} />
<RadioGroup options={defaultOptions} />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
const ids = radioButtons.map((button) => button.id);
const uniqueIds = new Set(ids);
// Should have unique IDs
expect(uniqueIds.size).toBe(6);
});
it("uses provided name for form association", () => {
render(<RadioGroup options={defaultOptions} name="test-group" />);
const hiddenInputs = screen.getAllByDisplayValue("option1");
hiddenInputs.forEach((input) => {
expect(input).toHaveAttribute("name", "test-group");
});
});
it("has proper form association", () => {
render(
<RadioGroup options={defaultOptions} name="test-group" value="option2" />,
);
const hiddenInputs = screen.getAllByDisplayValue("option1");
expect(hiddenInputs[0]).toHaveAttribute("name", "test-group");
expect(hiddenInputs[0]).toHaveAttribute("value", "option1");
expect(hiddenInputs[0]).not.toBeChecked();
const option2Inputs = screen.getAllByDisplayValue("option2");
expect(option2Inputs[0]).toHaveAttribute("name", "test-group");
expect(option2Inputs[0]).toHaveAttribute("value", "option2");
expect(option2Inputs[0]).toBeChecked();
});
it("maintains focus management", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
const { rerender } = render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
expect(radioButtons[1]).toHaveFocus();
// Change selection
rerender(
<RadioGroup
options={defaultOptions}
value="option2"
onChange={handleChange}
/>,
);
// Should still be focusable
expect(radioButtons[1]).toHaveAttribute("tabIndex", "0");
});
it("supports screen reader navigation", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
const radioButtons = screen.getAllByRole("radio");
// RadioGroup should be present
expect(radioGroup).toBeInTheDocument();
// All radio buttons should be in tab order
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
expect(button).toHaveAttribute("role", "radio");
});
});
it("handles empty options gracefully", () => {
render(<RadioGroup options={[]} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.queryAllByRole("radio");
expect(radioButtons).toHaveLength(0);
});
it("has proper accessible names", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button, index) => {
const labelId = button.getAttribute("aria-labelledby");
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent(`Option ${index + 1}`);
});
});
it("maintains single selection behavior", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
// Click option 2 directly
await user.click(radioButtons[1]);
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
// Only one should be selected at a time
expect(handleChange).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,249 @@
import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import Checkbox from "../../app/components/Checkbox";
// Test component that uses Checkbox in a form
function TestForm() {
const [formData, setFormData] = useState({
agree: false,
newsletter: false,
notifications: true,
});
const handleCheckboxChange =
(field) =>
({ checked }) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Form submission logic would go here
};
return (
<form onSubmit={handleSubmit} data-testid="test-form">
<Checkbox
label="I agree to the terms"
checked={formData.agree}
onChange={handleCheckboxChange("agree")}
name="agree"
data-testid="agree-checkbox"
/>
<Checkbox
label="Subscribe to newsletter"
checked={formData.newsletter}
onChange={handleCheckboxChange("newsletter")}
name="newsletter"
data-testid="newsletter-checkbox"
/>
<Checkbox
label="Enable notifications"
checked={formData.notifications}
onChange={handleCheckboxChange("notifications")}
name="notifications"
data-testid="notifications-checkbox"
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
}
describe("Checkbox Integration Tests", () => {
test("handles multiple checkboxes in a form", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const notificationsCheckbox = screen.getByTestId("notifications-checkbox");
// Initial state
expect(agreeCheckbox).toHaveAttribute("aria-checked", "false");
expect(newsletterCheckbox).toHaveAttribute("aria-checked", "false");
expect(notificationsCheckbox).toHaveAttribute("aria-checked", "true");
// Toggle checkboxes
await user.click(agreeCheckbox);
await user.click(newsletterCheckbox);
await user.click(notificationsCheckbox);
// Check final state
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
expect(newsletterCheckbox).toHaveAttribute("aria-checked", "true");
expect(notificationsCheckbox).toHaveAttribute("aria-checked", "false");
});
test("handles keyboard navigation between checkboxes", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const notificationsCheckbox = screen.getByTestId("notifications-checkbox");
// Focus first checkbox
await user.tab();
expect(agreeCheckbox).toHaveFocus();
// Navigate to next checkbox
await user.tab();
expect(newsletterCheckbox).toHaveFocus();
// Navigate to next checkbox
await user.tab();
expect(notificationsCheckbox).toHaveFocus();
});
test("handles keyboard activation", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
// Focus and activate with Space
await user.tab();
expect(agreeCheckbox).toHaveFocus();
expect(agreeCheckbox).toHaveAttribute("aria-checked", "false");
await user.keyboard(" ");
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
// Activate with Enter
await user.keyboard("Enter");
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
});
test("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [checked, setChecked] = useState(false);
return (
<div>
<Checkbox
label="Switch to inverse mode"
checked={mode === "inverse"}
onChange={({ checked }) =>
setMode(checked ? "inverse" : "standard")
}
data-testid="mode-switch"
/>
<Checkbox
label="Test checkbox"
checked={checked}
onChange={({ checked }) => setChecked(checked)}
mode={mode}
data-testid="test-checkbox"
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const modeSwitch = screen.getByTestId("mode-switch");
const testCheckbox = screen.getByTestId("test-checkbox");
// Initially standard mode
expect(testCheckbox).toBeInTheDocument();
// Switch to inverse mode
await user.click(modeSwitch);
expect(testCheckbox).toBeInTheDocument();
// Should still be functional
await user.click(testCheckbox);
expect(testCheckbox).toHaveAttribute("aria-checked", "true");
});
test("handles form submission with checkbox values", async () => {
const handleSubmit = vi.fn();
function FormWithSubmission() {
const [formData, setFormData] = useState({
agree: false,
newsletter: false,
});
const handleCheckboxChange =
(field) =>
({ checked }) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const onSubmit = (e) => {
e.preventDefault();
handleSubmit(formData);
};
return (
<form onSubmit={onSubmit} data-testid="form">
<Checkbox
label="I agree"
checked={formData.agree}
onChange={handleCheckboxChange("agree")}
name="agree"
value="yes"
data-testid="agree-checkbox"
/>
<Checkbox
label="Newsletter"
checked={formData.newsletter}
onChange={handleCheckboxChange("newsletter")}
name="newsletter"
value="yes"
data-testid="newsletter-checkbox"
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
}
const user = userEvent.setup();
render(<FormWithSubmission />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const submitButton = screen.getByTestId("submit-button");
// Check some checkboxes
await user.click(agreeCheckbox);
await user.click(newsletterCheckbox);
// Submit form
await user.click(submitButton);
// Verify form data was captured
expect(handleSubmit).toHaveBeenCalledWith({
agree: true,
newsletter: true,
});
});
test("handles accessibility in form context", async () => {
render(<TestForm />);
const form = screen.getByTestId("test-form");
const checkboxes = screen.getAllByRole("checkbox");
// All checkboxes should be accessible
expect(checkboxes).toHaveLength(3);
checkboxes.forEach((checkbox) => {
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked");
expect(checkbox).toHaveAttribute("tabIndex");
});
// Form should be accessible
expect(form).toBeInTheDocument();
});
});
@@ -0,0 +1,389 @@
import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
describe("ContextMenu Components Integration", () => {
const TestMenu = ({ onItemClick, selectedValue }) => (
<ContextMenu>
<ContextMenuSection title="Actions">
<ContextMenuItem
onClick={() => onItemClick("action1")}
selected={selectedValue === "action1"}
>
Action 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => onItemClick("action2")}
selected={selectedValue === "action2"}
>
Action 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Settings">
<ContextMenuItem
onClick={() => onItemClick("setting1")}
hasSubmenu={true}
>
Setting 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => onItemClick("setting2")}
disabled={true}
>
Setting 2
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
describe("Menu Interaction", () => {
it("handles item selection correctly", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const action1 = screen.getByText("Action 1");
await user.click(action1);
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("shows selected state correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="action1" />);
const action1 = screen.getByRole("menuitem", { name: "Action 1" });
expect(action1).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
);
});
it("handles disabled items correctly", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const setting2 = screen.getByText("Setting 2");
await user.click(setting2);
expect(onItemClick).not.toHaveBeenCalled();
});
it("shows submenu indicators correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const setting1 = screen.getByText("Setting 1");
const arrow = screen
.getByRole("menuitem", { name: "Setting 1" })
.querySelector("svg");
expect(arrow).toBeInTheDocument();
});
});
describe("Keyboard Navigation", () => {
it("navigates through menu items with arrow keys", async () => {
const user = userEvent.setup();
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(4);
// Check that enabled items are focusable and disabled items are not
const enabledItems = items.filter(
(item) =>
!item.hasAttribute("aria-disabled") ||
item.getAttribute("aria-disabled") !== "true",
);
const disabledItems = items.filter(
(item) => item.getAttribute("aria-disabled") === "true",
);
enabledItems.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
disabledItems.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "-1");
});
});
it("selects items with Enter key", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard("{Enter}");
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("selects items with Space key", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard(" ");
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("skips disabled items during navigation", async () => {
const user = userEvent.setup();
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(4);
// Check that disabled items have tabIndex="-1"
const disabledItem = screen.getByRole("menuitem", { name: "Setting 2" });
expect(disabledItem).toHaveAttribute("tabIndex", "-1");
expect(disabledItem).toHaveAttribute("aria-disabled", "true");
});
});
describe("Dynamic Menu Updates", () => {
const DynamicMenu = ({ items, selectedValue, onItemClick }) => (
<ContextMenu>
{items.map((item, index) => (
<ContextMenuItem
key={item.id}
onClick={() => onItemClick(item.id)}
selected={selectedValue === item.id}
disabled={item.disabled}
>
{item.label}
</ContextMenuItem>
))}
</ContextMenu>
);
it("handles dynamic item updates", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
const { rerender } = render(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
]}
selectedValue=""
onItemClick={onItemClick}
/>,
);
const item1 = screen.getByText("Item 1");
await user.click(item1);
expect(onItemClick).toHaveBeenCalledWith("1");
// Update items
rerender(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
{ id: "3", label: "Item 3" },
]}
selectedValue="1"
onItemClick={onItemClick}
/>,
);
expect(screen.getByText("Item 3")).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Item 1" })).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
);
});
it("handles item removal", () => {
const { rerender } = render(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
{ id: "3", label: "Item 3" },
]}
selectedValue="2"
onItemClick={vi.fn()}
/>,
);
expect(screen.getByText("Item 2")).toBeInTheDocument();
rerender(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "3", label: "Item 3" },
]}
selectedValue=""
onItemClick={vi.fn()}
/>,
);
expect(screen.queryByText("Item 2")).not.toBeInTheDocument();
});
});
describe("Menu State Management", () => {
const StatefulMenu = () => {
const [selectedValue, setSelectedValue] = useState("");
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Close Menu" : "Open Menu"}
</button>
{isOpen && (
<ContextMenu>
<ContextMenuItem
onClick={() => {
setSelectedValue("option1");
setIsOpen(false);
}}
selected={selectedValue === "option1"}
>
Option 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => {
setSelectedValue("option2");
setIsOpen(false);
}}
selected={selectedValue === "option2"}
>
Option 2
</ContextMenuItem>
</ContextMenu>
)}
</div>
);
};
it("manages menu open/close state", async () => {
const user = userEvent.setup();
render(<StatefulMenu />);
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
await user.click(toggleButton);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Close Menu" }),
).toBeInTheDocument();
});
it("closes menu after selection", async () => {
const user = userEvent.setup();
render(<StatefulMenu />);
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
await user.click(toggleButton);
const option1 = screen.getByText("Option 1");
await user.click(option1);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Open Menu" }),
).toBeInTheDocument();
});
});
describe("Performance", () => {
it("handles large menu lists efficiently", async () => {
const user = userEvent.setup();
const largeItems = Array.from({ length: 100 }, (_, i) => ({
id: `item${i}`,
label: `Item ${i}`,
}));
const LargeMenu = () => (
<ContextMenu>
{largeItems.map((item) => (
<ContextMenuItem key={item.id} onClick={vi.fn()}>
{item.label}
</ContextMenuItem>
))}
</ContextMenu>
);
render(<LargeMenu />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(100);
// Test that all items are focusable
items.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
});
it("handles rapid state changes", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={false}>
Item 1
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={false}>
Item 2
</ContextMenuItem>
</ContextMenu>,
);
// Rapidly change selection state
for (let i = 0; i < 10; i++) {
rerender(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={i % 2 === 0}>
Item 1
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={i % 2 === 1}>
Item 2
</ContextMenuItem>
</ContextMenu>,
);
}
// Should still be functional
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(2);
});
});
describe("Error Handling", () => {
it("handles missing onClick gracefully", () => {
render(
<ContextMenu>
<ContextMenuItem>Item without onClick</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByText("Item without onClick");
expect(item).toBeInTheDocument();
});
it("handles invalid props gracefully", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={null}>
Item with invalid selected
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByText("Item with invalid selected");
expect(item).toBeInTheDocument();
});
});
});
@@ -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-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
// 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,367 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../app/components/RadioButton";
describe("RadioButton Integration", () => {
it("works in form context", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
function TestForm() {
const [value, setValue] = useState("option1");
return (
<form onSubmit={handleSubmit}>
<RadioButton
label="Option 1"
name="test-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
<RadioButton
label="Option 2"
name="test-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
/>
<button type="submit">Submit</button>
</form>
);
}
render(<TestForm />);
const option1 = screen.getByText("Option 1").closest("label");
const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button");
// Initially option1 should be selected
expect(screen.getByDisplayValue("option1")).toBeChecked();
expect(screen.getByDisplayValue("option2")).not.toBeChecked();
// Click option2
await user.click(option2);
expect(screen.getByDisplayValue("option2")).toBeChecked();
expect(screen.getByDisplayValue("option1")).not.toBeChecked();
// Submit form
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
function KeyboardForm() {
const [value, setValue] = useState("option1");
return (
<div>
<RadioButton
label="Option 1"
name="keyboard-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
<RadioButton
label="Option 2"
name="keyboard-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
/>
</div>
);
}
render(<KeyboardForm />);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await user.tab();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(screen.getByDisplayValue("option2")).toBeChecked();
});
it("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [value, setValue] = useState("option1");
return (
<div>
<button
onClick={() =>
setMode(mode === "standard" ? "inverse" : "standard")
}
>
Toggle Mode
</button>
<RadioButton
label="Test Radio"
name="mode-radio"
value="option1"
checked={value === "option1"}
mode={mode}
onChange={({ checked }) => checked && setValue("option1")}
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const toggleButton = screen.getByRole("button");
const radioButton = screen.getByRole("radio");
// Initially standard mode
expect(radioButton).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
// Switch to inverse mode
await user.click(toggleButton);
expect(radioButton).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
it("maintains state across re-renders", () => {
function StateForm() {
const [value, setValue] = useState("option1");
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render ({count})
</button>
<RadioButton
label="Test Radio"
name="state-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
</div>
);
}
const user = userEvent.setup();
render(<StateForm />);
const radioButton = screen.getByRole("radio");
const reRenderButton = screen.getByRole("button");
// Should be checked initially
expect(radioButton).toHaveAttribute("aria-checked", "true");
// Re-render should maintain state
user.click(reRenderButton);
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("works with multiple radio groups", async () => {
function MultiGroupForm() {
const [group1Value, setGroup1Value] = useState("option1");
const [group2Value, setGroup2Value] = useState("option1");
return (
<div>
<div>
<h3>Group 1</h3>
<RadioButton
label="Option A"
name="group1"
value="option1"
checked={group1Value === "option1"}
onChange={({ checked }) => checked && setGroup1Value("option1")}
/>
<RadioButton
label="Option B"
name="group1"
value="option2"
checked={group1Value === "option2"}
onChange={({ checked }) => checked && setGroup1Value("option2")}
/>
</div>
<div>
<h3>Group 2</h3>
<RadioButton
label="Option X"
name="group2"
value="option1"
checked={group2Value === "option1"}
onChange={({ checked }) => checked && setGroup2Value("option1")}
/>
<RadioButton
label="Option Y"
name="group2"
value="option2"
checked={group2Value === "option2"}
onChange={({ checked }) => checked && setGroup2Value("option2")}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<MultiGroupForm />);
// Both groups should work independently
const group1OptionB = screen.getByText("Option B").closest("label");
const group2OptionY = screen.getByText("Option Y").closest("label");
await user.click(group1OptionB);
await user.click(group2OptionY);
const group1Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group1");
const group2Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group2");
expect(group1Inputs[0]).toBeChecked();
expect(group2Inputs[0]).toBeChecked();
});
it("handles controlled and uncontrolled scenarios", async () => {
function ControlledForm() {
const [controlledValue, setControlledValue] = useState("option1");
const [uncontrolledValue, setUncontrolledValue] = useState("option1");
return (
<div>
<div>
<h3>Controlled</h3>
<RadioButton
label="Controlled Option 1"
name="controlled"
value="option1"
checked={controlledValue === "option1"}
onChange={({ checked }) =>
checked && setControlledValue("option1")
}
/>
<RadioButton
label="Controlled Option 2"
name="controlled"
value="option2"
checked={controlledValue === "option2"}
onChange={({ checked }) =>
checked && setControlledValue("option2")
}
/>
</div>
<div>
<h3>Uncontrolled</h3>
<RadioButton
label="Uncontrolled Option 1"
name="uncontrolled"
value="option1"
checked={uncontrolledValue === "option1"}
onChange={({ checked }) =>
checked && setUncontrolledValue("option1")
}
/>
<RadioButton
label="Uncontrolled Option 2"
name="uncontrolled"
value="option2"
checked={uncontrolledValue === "option2"}
onChange={({ checked }) =>
checked && setUncontrolledValue("option2")
}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<ControlledForm />);
// Both should work the same way
const controlledOption2 = screen
.getByText("Controlled Option 2")
.closest("label");
const uncontrolledOption2 = screen
.getByText("Uncontrolled Option 2")
.closest("label");
await user.click(controlledOption2);
await user.click(uncontrolledOption2);
const controlledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "controlled");
const uncontrolledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "uncontrolled");
expect(controlledInputs[0]).toBeChecked();
expect(uncontrolledInputs[0]).toBeChecked();
});
it("handles accessibility in complex forms", () => {
function AccessibleForm() {
const [value, setValue] = useState("option1");
return (
<form>
<fieldset>
<legend>Choose an option</legend>
<RadioButton
label="Option 1"
name="accessible-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
ariaLabel="First option"
/>
<RadioButton
label="Option 2"
name="accessible-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
ariaLabel="Second option"
/>
</fieldset>
</form>
);
}
render(<AccessibleForm />);
const radioButtons = screen.getAllByRole("radio");
// Should have proper accessibility attributes
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have aria-labels
expect(radioButtons[0]).toHaveAttribute("aria-label", "First option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second option");
});
});
@@ -0,0 +1,431 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../app/components/RadioGroup";
describe("RadioGroup Integration", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("works in form context", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
function TestForm() {
const [value, setValue] = useState("option1");
return (
<form onSubmit={handleSubmit}>
<RadioGroup
name="test-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
<button type="submit">Submit</button>
</form>
);
}
render(<TestForm />);
const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button");
// Initially option1 should be selected
expect(screen.getByDisplayValue("option1")).toBeChecked();
expect(screen.getByDisplayValue("option2")).not.toBeChecked();
// Click option2
await user.click(option2);
expect(screen.getByDisplayValue("option2")).toBeChecked();
expect(screen.getByDisplayValue("option1")).not.toBeChecked();
// Submit form
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
function KeyboardForm() {
const [value, setValue] = useState("option1");
return (
<RadioGroup
name="keyboard-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
);
}
render(<KeyboardForm />);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await user.tab();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(screen.getByDisplayValue("option2")).toBeChecked();
});
it("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [value, setValue] = useState("option1");
return (
<div>
<button
onClick={() =>
setMode(mode === "standard" ? "inverse" : "standard")
}
>
Toggle Mode
</button>
<RadioGroup
name="mode-radio-group"
value={value}
mode={mode}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const toggleButton = screen.getByRole("button");
const radioButtons = screen.getAllByRole("radio");
// Initially standard mode
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
// Switch to inverse mode
await user.click(toggleButton);
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
});
it("maintains state across re-renders", () => {
function StateForm() {
const [value, setValue] = useState("option1");
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render ({count})
</button>
<RadioGroup
name="state-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<StateForm />);
const radioButtons = screen.getAllByRole("radio");
const reRenderButton = screen.getByRole("button");
// Should be checked initially
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
// Re-render should maintain state
user.click(reRenderButton);
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
});
it("works with multiple radio groups", async () => {
function MultiGroupForm() {
const [group1Value, setGroup1Value] = useState("option1");
const [group2Value, setGroup2Value] = useState("option1");
return (
<div>
<div>
<h3>Group 1</h3>
<RadioGroup
name="group1"
value={group1Value}
options={defaultOptions}
onChange={({ value }) => setGroup1Value(value)}
/>
</div>
<div>
<h3>Group 2</h3>
<RadioGroup
name="group2"
value={group2Value}
options={defaultOptions}
onChange={({ value }) => setGroup2Value(value)}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<MultiGroupForm />);
// Both groups should work independently
// Find the Option 2 in group1 by filtering getAllByDisplayValue by name
const group1Option2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "group1");
const group1Option2 = group1Option2Input.closest("label");
// Find the Option 3 in group2 by filtering getAllByDisplayValue by name
const group2Option3Input = screen
.getAllByDisplayValue("option3")
.find((input) => input.getAttribute("name") === "group2");
const group2Option3 = group2Option3Input.closest("label");
await user.click(group1Option2);
await user.click(group2Option3);
const group1Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group1");
const group2Inputs = screen
.getAllByDisplayValue("option3")
.filter((input) => input.getAttribute("name") === "group2");
expect(group1Inputs[0]).toBeChecked();
expect(group2Inputs[0]).toBeChecked();
});
it("handles controlled and uncontrolled scenarios", async () => {
function ControlledForm() {
const [controlledValue, setControlledValue] = useState("option1");
const [uncontrolledValue, setUncontrolledValue] = useState("option1");
return (
<div>
<div>
<h3>Controlled</h3>
<RadioGroup
name="controlled"
value={controlledValue}
options={defaultOptions}
onChange={({ value }) => setControlledValue(value)}
/>
</div>
<div>
<h3>Uncontrolled</h3>
<RadioGroup
name="uncontrolled"
value={uncontrolledValue}
options={defaultOptions}
onChange={({ value }) => setUncontrolledValue(value)}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<ControlledForm />);
// Both should work the same way
// Find the Option 2 in controlled group by filtering getAllByDisplayValue by name
const controlledOption2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "controlled");
const controlledOption2 = controlledOption2Input.closest("label");
// Find the Option 2 in uncontrolled group by filtering getAllByDisplayValue by name
const uncontrolledOption2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "uncontrolled");
const uncontrolledOption2 = uncontrolledOption2Input.closest("label");
await user.click(controlledOption2);
await user.click(uncontrolledOption2);
const controlledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "controlled");
const uncontrolledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "uncontrolled");
expect(controlledInputs[0]).toBeChecked();
expect(uncontrolledInputs[0]).toBeChecked();
});
it("handles accessibility in complex forms", () => {
function AccessibleForm() {
const [value, setValue] = useState("option1");
const accessibleOptions = [
{ value: "option1", label: "Option 1", ariaLabel: "First option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second option" },
{ value: "option3", label: "Option 3", ariaLabel: "Third option" },
];
return (
<form>
<fieldset>
<legend>Choose an option</legend>
<RadioGroup
name="accessible-radio-group"
value={value}
options={accessibleOptions}
onChange={({ value }) => setValue(value)}
aria-label="Accessible radio group"
/>
</fieldset>
</form>
);
}
render(<AccessibleForm />);
const radioGroup = screen.getByRole("radiogroup");
const radioButtons = screen.getAllByRole("radio");
// Should have proper accessibility attributes
expect(radioGroup).toHaveAttribute("aria-label", "Accessible radio group");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have aria-labels
expect(radioButtons[0]).toHaveAttribute("aria-label", "First option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second option");
expect(radioButtons[2]).toHaveAttribute("aria-label", "Third option");
});
it("handles dynamic options", async () => {
function DynamicForm() {
const [value, setValue] = useState("option1");
const [options, setOptions] = useState(defaultOptions);
return (
<div>
<button
onClick={() =>
setOptions([...options, { value: "option4", label: "Option 4" }])
}
>
Add Option
</button>
<RadioGroup
name="dynamic-radio-group"
value={value}
options={options}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<DynamicForm />);
const addButton = screen.getByRole("button");
// Initially 3 options
expect(screen.getAllByRole("radio")).toHaveLength(3);
// Add option
await user.click(addButton);
expect(screen.getAllByRole("radio")).toHaveLength(4);
expect(screen.getByText("Option 4")).toBeInTheDocument();
});
it("handles empty options gracefully", () => {
function EmptyForm() {
const [value, setValue] = useState("");
return (
<RadioGroup
name="empty-radio-group"
value={value}
options={[]}
onChange={({ value }) => setValue(value)}
/>
);
}
render(<EmptyForm />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
expect(screen.queryAllByRole("radio")).toHaveLength(0);
});
it("maintains single selection behavior", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
function SingleSelectionForm() {
const [value, setValue] = useState("option1");
return (
<RadioGroup
name="single-selection-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => {
setValue(value);
handleChange(value);
}}
/>
);
}
render(<SingleSelectionForm />);
const radioButtons = screen.getAllByRole("radio");
// Initially option1 should be selected
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click option2
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
// Only option2 should be selected
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
expect(handleChange).toHaveBeenCalledWith("option2");
});
});
@@ -0,0 +1,407 @@
import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest";
import Select from "../../app/components/Select";
describe("Select Component Integration", () => {
const TestForm = ({ initialValue = "" }) => {
const [value, setValue] = useState(initialValue);
const [errors, setErrors] = useState({});
const handleChange = (newValue) => {
setValue(newValue);
if (errors.select) {
setErrors({ ...errors, select: null });
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (!value) {
setErrors({ select: "Please select an option" });
}
};
return (
<form onSubmit={handleSubmit}>
<Select
label="Test Select"
placeholder="Select an option"
value={value}
onChange={handleChange}
error={!!errors.select}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
{errors.select && <div data-testid="error">{errors.select}</div>}
<button type="submit">Submit</button>
</form>
);
};
describe("Form Integration", () => {
it("integrates with form submission", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
it("shows validation error when no option selected", async () => {
const user = userEvent.setup();
render(<TestForm />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.getByTestId("error")).toHaveTextContent(
"Please select an option",
);
});
it("clears error when option is selected", async () => {
const user = userEvent.setup();
render(<TestForm />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.getByTestId("error")).toBeInTheDocument();
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
});
describe("Multiple Select Components", () => {
const MultiSelectForm = () => {
const [values, setValues] = useState({ select1: "", select2: "" });
const handleChange = (field) => (newValue) => {
setValues({ ...values, [field]: newValue });
};
return (
<div>
<Select
label="First Select"
placeholder="Select first option"
value={values.select1}
onChange={handleChange("select1")}
options={[
{ value: "a1", label: "A1" },
{ value: "a2", label: "A2" },
]}
/>
<Select
label="Second Select"
placeholder="Select second option"
value={values.select2}
onChange={handleChange("select2")}
options={[
{ value: "b1", label: "B1" },
{ value: "b2", label: "B2" },
]}
/>
</div>
);
};
it("handles multiple select components independently", async () => {
const user = userEvent.setup();
render(<MultiSelectForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.click(firstSelect);
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
await user.click(screen.getByText("A1"));
await user.click(secondSelect);
await waitFor(() => {
expect(screen.getByText("B1")).toBeInTheDocument();
});
await user.click(screen.getByText("B1"));
expect(firstSelect).toHaveTextContent("A1");
expect(secondSelect).toHaveTextContent("B1");
});
it("closes one dropdown when another is opened", async () => {
const user = userEvent.setup();
render(<MultiSelectForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.click(firstSelect);
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
await user.click(secondSelect);
await waitFor(() => {
expect(screen.queryByText("A1")).not.toBeInTheDocument();
expect(screen.getByText("B1")).toBeInTheDocument();
});
});
});
describe("Keyboard Navigation Between Components", () => {
const KeyboardForm = () => {
const [values, setValues] = useState({ select1: "", select2: "" });
return (
<div>
<input placeholder="First input" />
<Select
label="First Select"
placeholder="Select first option"
value={values.select1}
onChange={(value) => setValues({ ...values, select1: value })}
options={[{ value: "a1", label: "A1" }]}
/>
<input placeholder="Second input" />
<Select
label="Second Select"
placeholder="Select second option"
value={values.select2}
onChange={(value) => setValues({ ...values, select2: value })}
options={[{ value: "b1", label: "B1" }]}
/>
</div>
);
};
it("handles keyboard navigation between inputs and selects", async () => {
const user = userEvent.setup();
render(<KeyboardForm />);
const firstInput = screen.getByPlaceholderText("First input");
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondInput = screen.getByPlaceholderText("Second input");
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.tab();
expect(firstInput).toHaveFocus();
await user.tab();
expect(firstSelect).toHaveFocus();
await user.tab();
expect(secondInput).toHaveFocus();
await user.tab();
expect(secondSelect).toHaveFocus();
});
it("opens select with Enter key during tab navigation", async () => {
const user = userEvent.setup();
render(<KeyboardForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
await user.tab();
await user.tab();
expect(firstSelect).toHaveFocus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
});
});
describe("Dynamic Prop Changes", () => {
const DynamicSelect = ({ disabled, error, size }) => {
const [value, setValue] = useState("");
return (
<Select
label="Dynamic Select"
placeholder="Select an option"
value={value}
onChange={setValue}
disabled={disabled}
error={error}
size={size}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
]}
/>
);
};
it("handles dynamic disabled state changes", async () => {
const { rerender } = render(<DynamicSelect disabled={false} />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).not.toBeDisabled();
rerender(<DynamicSelect disabled={true} />);
expect(selectButton).toBeDisabled();
rerender(<DynamicSelect disabled={false} />);
expect(selectButton).not.toBeDisabled();
});
it("handles dynamic error state changes", async () => {
const { rerender } = render(<DynamicSelect error={false} />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
rerender(<DynamicSelect error={true} />);
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
rerender(<DynamicSelect error={false} />);
expect(selectButton).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
it("handles dynamic size changes", async () => {
const { rerender } = render(<DynamicSelect size="small" />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).toHaveClass("h-[32px]");
rerender(<DynamicSelect size="medium" />);
expect(selectButton).toHaveClass("h-[36px]");
rerender(<DynamicSelect size="large" />);
expect(selectButton).toHaveClass("h-[40px]");
});
});
describe("Focus State Behavior", () => {
it("enters focus state when tabbed to (not active state)", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.tab();
expect(selectButton).toHaveFocus();
// Should have focus state styling, not active state
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
it("does not enter focus state when clicked", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
expect(selectButton).toHaveFocus();
// Click should not trigger focus-visible styles (class is always present but only active on keyboard focus)
// The focus-visible class is always in the component but only applies on keyboard focus
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
});
describe("Performance", () => {
it("handles rapid state changes without issues", async () => {
const user = userEvent.setup();
const { rerender } = render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
// Rapidly change props
for (let i = 0; i < 10; i++) {
rerender(<TestForm />);
await user.click(selectButton);
await user.keyboard("{Escape}");
}
// Should still be functional
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("handles large option lists efficiently", async () => {
const user = userEvent.setup();
const largeOptions = Array.from({ length: 100 }, (_, i) => ({
value: `option${i}`,
label: `Option ${i}`,
}));
render(
<Select
label="Large Select"
placeholder="Select an option"
options={largeOptions}
/>,
);
const selectButton = screen.getByRole("button", { name: /Large Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 0")).toBeInTheDocument();
expect(screen.getByText("Option 99")).toBeInTheDocument();
});
});
});
});
@@ -0,0 +1,265 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import Switch from "../../app/components/Switch";
// Test form component
const TestForm = ({ onSubmit }) => {
const [switch1, setSwitch1] = React.useState(false);
const [switch2, setSwitch2] = React.useState(true);
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ switch1, switch2 });
};
return (
<form onSubmit={handleSubmit}>
<Switch
checked={switch1}
onChange={() => setSwitch1(!switch1)}
label="First Switch"
/>
<Switch
checked={switch2}
onChange={() => setSwitch2(!switch2)}
label="Second Switch"
/>
<button type="submit">Submit</button>
</form>
);
};
// Dynamic switch component
const DynamicSwitch = ({ initialState = false }) => {
const [checked, setChecked] = React.useState(initialState);
// Update state when initialState prop changes
React.useEffect(() => {
setChecked(initialState);
}, [initialState]);
return (
<div>
<Switch
checked={checked}
onChange={() => setChecked(!checked)}
label="Dynamic Switch"
/>
</div>
);
};
describe("Switch Integration", () => {
it("handles form submission", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<TestForm onSubmit={handleSubmit} />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalledWith({
switch1: false,
switch2: true,
});
});
it("handles keyboard navigation between switches", async () => {
const user = userEvent.setup();
render(
<div>
<Switch label="First Switch" />
<Switch label="Second Switch" />
<Switch label="Third Switch" />
</div>,
);
const switches = screen.getAllByRole("switch");
expect(switches).toHaveLength(3);
// Focus first switch
await user.tab();
expect(switches[0]).toHaveFocus();
// Tab to second switch
await user.tab();
expect(switches[1]).toHaveFocus();
// Tab to third switch
await user.tab();
expect(switches[2]).toHaveFocus();
});
it("handles dynamic prop changes", () => {
const { rerender } = render(<DynamicSwitch initialState={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
// Change initial state - the DynamicSwitch component should handle this internally
rerender(<DynamicSwitch initialState={true} />);
switchButton = screen.getByRole("switch");
// The DynamicSwitch component manages its own state, so it should be checked
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles multiple switches in form", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
const TestForm = () => {
const [switch1, setSwitch1] = React.useState(false);
const [switch2, setSwitch2] = React.useState(false);
const [switch3, setSwitch3] = React.useState(false);
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<Switch
label="Switch 1"
checked={switch1}
onChange={() => setSwitch1(!switch1)}
/>
<Switch
label="Switch 2"
checked={switch2}
onChange={() => setSwitch2(!switch2)}
/>
<Switch
label="Switch 3"
checked={switch3}
onChange={() => setSwitch3(!switch3)}
/>
<button type="submit">Submit</button>
</form>
);
};
render(<TestForm />);
const switches = screen.getAllByRole("switch");
expect(switches).toHaveLength(3);
// Toggle first switch
await user.click(switches[0]);
expect(switches[0]).toHaveAttribute("aria-checked", "true");
// Toggle second switch
await user.click(switches[1]);
expect(switches[1]).toHaveAttribute("aria-checked", "true");
// Submit form
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles state changes", async () => {
const user = userEvent.setup();
const TestComponent = () => {
const [checked, setChecked] = React.useState(false);
return (
<div>
<Switch
checked={checked}
onChange={() => setChecked(!checked)}
label="Test Switch"
/>
</div>
);
};
render(<TestComponent />);
const switchButton = screen.getByRole("switch");
// Initially unchecked
expect(switchButton).toHaveAttribute("aria-checked", "false");
// Toggle checked state
await user.click(switchButton);
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles content changes", () => {
const { rerender } = render(<Switch label="Original Label" />);
expect(screen.getByText("Original Label")).toBeInTheDocument();
rerender(<Switch label="Updated Label" />);
expect(screen.getByText("Updated Label")).toBeInTheDocument();
expect(screen.queryByText("Original Label")).not.toBeInTheDocument();
});
it("handles performance with many switches", () => {
const switches = Array.from({ length: 100 }, (_, i) => (
<Switch key={i} label={`Switch ${i + 1}`} />
));
const startTime = performance.now();
render(<div>{switches}</div>);
const endTime = performance.now();
// Should render within reasonable time (less than 1 second)
expect(endTime - startTime).toBeLessThan(1000);
const renderedSwitches = screen.getAllByRole("switch");
expect(renderedSwitches).toHaveLength(100);
});
it("handles rapid state changes", async () => {
const user = userEvent.setup();
const TestComponent = () => {
const [checked, setChecked] = React.useState(false);
return (
<Switch
checked={checked}
onChange={() => setChecked(!checked)}
label="Rapid Toggle Switch"
/>
);
};
render(<TestComponent />);
const switchButton = screen.getByRole("switch");
// Rapidly toggle the switch
for (let i = 0; i < 10; i++) {
await user.click(switchButton);
await waitFor(() => {
expect(switchButton).toHaveAttribute(
"aria-checked",
i % 2 === 0 ? "true" : "false",
);
});
}
});
it("handles mixed content types", () => {
render(
<div>
<Switch label="Text Switch" />
<Switch label="Another Text Switch" />
<Switch />
<Switch label="Final Switch" />
</div>,
);
const switches = screen.getAllByRole("switch");
expect(switches).toHaveLength(4);
// Check that labels are rendered correctly
expect(screen.getByText("Text Switch")).toBeInTheDocument();
expect(screen.getByText("Another Text Switch")).toBeInTheDocument();
expect(screen.getByText("Final Switch")).toBeInTheDocument();
});
});
@@ -0,0 +1,280 @@
import React from "react";
import { expect, test, describe, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
// Test form component for integration testing
const TestForm = () => {
const [formData, setFormData] = React.useState({
textarea1: "",
textarea2: "",
});
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<TextArea
label="First TextArea"
name="textarea1"
value={formData.textarea1}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea1: e.target.value }))
}
placeholder="Enter first text..."
/>
<TextArea
label="Second TextArea"
name="textarea2"
value={formData.textarea2}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea2: e.target.value }))
}
placeholder="Enter second text..."
/>
<button type="submit">Submit</button>
</form>
);
};
// Dynamic TextArea component for prop changes testing
const DynamicTextArea = ({ size, labelVariant, state, disabled, error }) => {
const [value, setValue] = React.useState("");
return (
<TextArea
label="Dynamic TextArea"
value={value}
onChange={(e) => setValue(e.target.value)}
size={size}
labelVariant={labelVariant}
state={state}
disabled={disabled}
error={error}
placeholder="Enter text..."
/>
);
};
describe("TextArea Integration Tests", () => {
test("handles form submission with multiple textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
const submitButton = screen.getByRole("button", { name: /Submit/ });
await user.type(firstTextarea, "First content");
await user.type(secondTextarea, "Second content");
expect(firstTextarea).toHaveValue("First content");
expect(secondTextarea).toHaveValue("Second content");
await user.click(submitButton);
// Form submission should not cause errors
});
test("handles keyboard navigation between textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
await user.click(firstTextarea);
expect(firstTextarea).toHaveFocus();
await user.tab();
expect(secondTextarea).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<DynamicTextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<DynamicTextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<DynamicTextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("handles state changes", () => {
const { rerender } = render(<DynamicTextArea state="default" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]",
);
});
test("handles disabled state changes", () => {
const { rerender } = render(<DynamicTextArea disabled={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).not.toBeDisabled();
rerender(<DynamicTextArea disabled={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("handles error state changes", () => {
const { rerender } = render(<DynamicTextArea error={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea error={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("handles label variant changes", () => {
const { rerender } = render(<DynamicTextArea labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
rerender(<DynamicTextArea labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("handles text input and changes", async () => {
const user = userEvent.setup();
render(<DynamicTextArea />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "Hello World");
expect(textarea).toHaveValue("Hello World");
});
test("handles focus and blur events", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
const handleBlur = vi.fn();
render(
<TextArea
label="Test TextArea"
onFocus={handleFocus}
onBlur={handleBlur}
/>,
);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("handles multiple textareas with different configurations", () => {
render(
<div>
<TextArea
size="small"
label="Small TextArea"
placeholder="Small placeholder"
/>
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
placeholder="Medium placeholder"
/>
<TextArea
size="large"
label="Large TextArea"
placeholder="Large placeholder"
/>
</div>,
);
expect(
screen.getByPlaceholderText("Small placeholder"),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Medium placeholder"),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Large placeholder"),
).toBeInTheDocument();
});
test("handles form validation with error states", () => {
render(
<div>
<TextArea label="Valid TextArea" placeholder="Valid input" />
<TextArea label="Invalid TextArea" placeholder="Invalid input" error />
<TextArea
label="Disabled TextArea"
placeholder="Disabled input"
disabled
/>
</div>,
);
const validTextarea = screen.getByPlaceholderText("Valid input");
const invalidTextarea = screen.getByPlaceholderText("Invalid input");
const disabledTextarea = screen.getByPlaceholderText("Disabled input");
expect(validTextarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
expect(invalidTextarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
expect(disabledTextarea).toBeDisabled();
});
test("handles performance with multiple re-renders", () => {
const { rerender } = render(<DynamicTextArea />);
// Simulate multiple re-renders
for (let i = 0; i < 10; i++) {
rerender(<DynamicTextArea size={i % 2 === 0 ? "small" : "large"} />);
}
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("handles accessibility with screen readers", async () => {
const user = userEvent.setup();
render(<TextArea label="Accessible TextArea" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Accessible TextArea");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
await user.click(textarea);
expect(textarea).toHaveFocus();
});
});
@@ -0,0 +1,185 @@
import React from "react";
import { expect, test, describe, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Toggle from "../../app/components/Toggle";
describe("Toggle Integration", () => {
test("handles form submission", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Toggle label="Test Toggle" name="toggle" />
<button type="submit">Submit</button>
</form>,
);
const toggle = screen.getByRole("switch", { name: "Test Toggle" });
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(toggle);
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
test("handles keyboard navigation between toggles", async () => {
const user = userEvent.setup();
render(
<div>
<Toggle label="First Toggle" />
<Toggle label="Second Toggle" />
<Toggle label="Third Toggle" />
</div>,
);
const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
const thirdToggle = screen.getByRole("switch", { name: "Third Toggle" });
await user.tab();
expect(firstToggle).toHaveFocus();
await user.tab();
expect(secondToggle).toHaveFocus();
await user.tab();
expect(thirdToggle).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
rerender(<Toggle label="Test Toggle" disabled={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
});
test("handles multiple toggles in form", () => {
const handleChange1 = vi.fn();
const handleChange2 = vi.fn();
render(
<div>
<Toggle label="First Toggle" onChange={handleChange1} />
<Toggle label="Second Toggle" onChange={handleChange2} />
</div>,
);
const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
fireEvent.click(firstToggle);
expect(handleChange1).toHaveBeenCalledTimes(1);
expect(handleChange2).not.toHaveBeenCalled();
fireEvent.click(secondToggle);
expect(handleChange2).toHaveBeenCalledTimes(1);
expect(handleChange1).toHaveBeenCalledTimes(1);
});
test("handles state changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" state="default" />);
let toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Toggle label="Test Toggle" state="focus" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
test("handles content changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" />);
let toggle = screen.getByRole("switch");
expect(toggle).not.toHaveTextContent("I");
expect(toggle).not.toHaveTextContent("Toggle");
rerender(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
rerender(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("Toggle");
rerender(
<Toggle
label="Test Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>,
);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
expect(toggle).toHaveTextContent("Toggle");
});
test("handles performance with many toggles", () => {
const toggles = Array.from({ length: 100 }, (_, i) => (
<Toggle key={i} label={`Toggle ${i}`} />
));
const startTime = performance.now();
render(<div>{toggles}</div>);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(1000); // Should render in less than 1 second
expect(screen.getAllByRole("switch")).toHaveLength(100);
});
test("handles rapid state changes", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
// Rapid clicks
for (let i = 0; i < 10; i++) {
fireEvent.click(toggle);
}
expect(handleChange).toHaveBeenCalledTimes(10);
});
test("handles mixed content types", () => {
render(
<div>
<Toggle label="Icon Toggle" showIcon={true} icon="I" />
<Toggle label="Text Toggle" showText={true} text="Toggle" />
<Toggle
label="Both Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>
<Toggle label="Empty Toggle" />
</div>,
);
const iconToggle = screen.getByRole("switch", { name: "Icon Toggle" });
const textToggle = screen.getByRole("switch", { name: "Text Toggle" });
const bothToggle = screen.getByRole("switch", { name: "Both Toggle" });
const emptyToggle = screen.getByRole("switch", { name: "Empty Toggle" });
expect(iconToggle).toHaveTextContent("I");
expect(textToggle).toHaveTextContent("Toggle");
expect(bothToggle).toHaveTextContent("I");
expect(bothToggle).toHaveTextContent("Toggle");
expect(emptyToggle).not.toHaveTextContent("I");
expect(emptyToggle).not.toHaveTextContent("Toggle");
});
});
@@ -0,0 +1,219 @@
import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ToggleGroup from "../../app/components/ToggleGroup";
// Test component for form integration
const TestForm = () => {
const [selectedToggle, setSelectedToggle] = useState("left");
return (
<form>
<div className="flex">
<ToggleGroup
position="left"
state={selectedToggle === "left" ? "selected" : "default"}
onChange={() => setSelectedToggle("left")}
>
Left Option
</ToggleGroup>
<ToggleGroup
position="middle"
state={selectedToggle === "middle" ? "selected" : "default"}
onChange={() => setSelectedToggle("middle")}
>
Middle Option
</ToggleGroup>
<ToggleGroup
position="right"
state={selectedToggle === "right" ? "selected" : "default"}
onChange={() => setSelectedToggle("right")}
>
Right Option
</ToggleGroup>
</div>
</form>
);
};
// Dynamic component for prop changes
const DynamicToggleGroup = ({ position, state, showText }) => {
return (
<ToggleGroup position={position} state={state} showText={showText}>
Dynamic Content
</ToggleGroup>
);
};
describe("ToggleGroup Integration", () => {
it("handles form submission", async () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<div className="flex">
<ToggleGroup position="left" onChange={() => {}}>
First Option
</ToggleGroup>
<ToggleGroup position="middle" onChange={() => {}}>
Second Option
</ToggleGroup>
<ToggleGroup position="right" onChange={() => {}}>
Third Option
</ToggleGroup>
</div>
<button type="submit">Submit</button>
</form>,
);
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
it("handles keyboard navigation between toggle groups", () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Focus first toggle group
toggleGroups[0].focus();
expect(toggleGroups[0]).toHaveFocus();
// Test keyboard navigation
fireEvent.keyDown(toggleGroups[0], { key: "Tab" });
// Note: Tab navigation behavior depends on browser implementation
});
it("handles dynamic prop changes", () => {
const { rerender } = render(
<DynamicToggleGroup position="left" state="default" showText={true} />,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
expect(toggleGroup).toHaveTextContent("Dynamic Content");
rerender(
<DynamicToggleGroup
position="middle"
state="selected"
showText={false}
/>,
);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
expect(toggleGroup).toHaveTextContent("Dynamic Content");
});
it("handles multiple toggle groups in form", () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups).toHaveLength(3);
// Test clicking different toggle groups
fireEvent.click(toggleGroups[0]);
fireEvent.click(toggleGroups[1]);
fireEvent.click(toggleGroups[2]);
});
it("handles state changes", async () => {
const { rerender } = render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Initially, left should be selected
expect(toggleGroups[0]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
// Click middle toggle
fireEvent.click(toggleGroups[1]);
await waitFor(() => {
expect(toggleGroups[1]).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
);
});
});
it("handles content changes", () => {
const { rerender } = render(
<ToggleGroup showText={true}>Initial Content</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Initial Content");
rerender(<ToggleGroup showText={true}>Updated Content</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Updated Content");
rerender(<ToggleGroup showText={false}>Hidden Content</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Hidden Content");
});
it("handles performance with many toggle groups", () => {
const ManyToggleGroups = () => {
const [selected, setSelected] = useState(0);
return (
<div className="flex">
{Array.from({ length: 10 }, (_, i) => (
<ToggleGroup
key={i}
position={i === 0 ? "left" : i === 9 ? "right" : "middle"}
state={selected === i ? "selected" : "default"}
onChange={() => setSelected(i)}
>
Option {i + 1}
</ToggleGroup>
))}
</div>
);
};
render(<ManyToggleGroups />);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups).toHaveLength(10);
// Test clicking different toggle groups
fireEvent.click(toggleGroups[5]);
expect(toggleGroups[5]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
});
it("handles rapid state changes", async () => {
const { rerender } = render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Rapidly change states
for (let i = 0; i < 5; i++) {
fireEvent.click(toggleGroups[i % 3]);
await waitFor(() => {
expect(toggleGroups[i % 3]).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
);
});
}
});
it("handles mixed content types", () => {
render(
<div className="flex">
<ToggleGroup position="left" showText={true}>
Text Only
</ToggleGroup>
<ToggleGroup position="middle" showText={false}>
Icon Only
</ToggleGroup>
<ToggleGroup position="right" showText={true}>
Text Only
</ToggleGroup>
</div>,
);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups[0]).toHaveTextContent("Text Only");
expect(toggleGroups[1]).toHaveTextContent("Icon Only");
expect(toggleGroups[2]).toHaveTextContent("Text Only");
});
});
@@ -0,0 +1,136 @@
import { within, userEvent } from "@storybook/test";
import { expect } from "@storybook/test";
// Interaction test for Default story
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test initial state
expect(checkbox).toHaveAttribute("aria-checked", "false");
// Test click interaction
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test toggle back
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Checked story
export const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test initial checked state
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test unchecking
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Standard story
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
// Test both checkboxes
expect(checkboxes).toHaveLength(2);
// Test first checkbox (unchecked)
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
// Test second checkbox (checked)
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Inverse story
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
// Test both checkboxes in inverse mode
expect(checkboxes).toHaveLength(2);
// Test first checkbox (unchecked)
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
// Test second checkbox (checked)
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
// Keyboard interaction test
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Focus the checkbox
await userEvent.tab();
expect(checkbox).toHaveFocus();
// Test Space key
await userEvent.keyboard(" ");
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test Enter key
await userEvent.keyboard("Enter");
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Accessibility interaction test
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test ARIA attributes
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked");
expect(checkbox).toHaveAttribute("tabIndex");
// Test keyboard navigation
await userEvent.tab();
expect(checkbox).toHaveFocus();
// Test activation
await userEvent.keyboard(" ");
expect(checkbox).toHaveAttribute("aria-checked", "true");
},
};
// Form integration test
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test form integration
const hiddenInput = canvas.getByRole("checkbox", { hidden: true });
expect(hiddenInput).toBeInTheDocument();
// Test checkbox interaction
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(hiddenInput).toBeChecked();
},
};
+234
View File
@@ -0,0 +1,234 @@
import { test, expect } from "@playwright/test";
test.describe("Checkbox Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:6006");
});
test("should load Checkbox stories", async ({ page }) => {
// Navigate to Checkbox stories
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Check that the stories are loaded
await expect(page.locator('[data-testid="Default"]')).toBeVisible();
await expect(page.locator('[data-testid="Checked"]')).toBeVisible();
await expect(page.locator('[data-testid="Standard"]')).toBeVisible();
await expect(page.locator('[data-testid="Inverse"]')).toBeVisible();
});
test("Default story should render correctly", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check that the checkbox is rendered
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("Checked story should render correctly", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Checked"]');
// Check that the checkbox is checked
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
});
test("Standard story should show standard mode checkboxes", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Standard"]');
// Check that multiple checkboxes are rendered
const checkboxes = page.locator('[role="checkbox"]');
await expect(checkboxes).toHaveCount(2); // Unchecked and checked
// Check that they have proper styling (standard mode)
const firstCheckbox = checkboxes.first();
await expect(firstCheckbox).toBeVisible();
});
test("Inverse story should show inverse mode checkboxes", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Inverse"]');
// Check that multiple checkboxes are rendered
const checkboxes = page.locator('[role="checkbox"]');
await expect(checkboxes).toHaveCount(2); // Unchecked and checked
// Check that they have proper styling (inverse mode)
const firstCheckbox = checkboxes.first();
await expect(firstCheckbox).toBeVisible();
});
test("should have proper controls in Controls panel", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check that controls are available
await expect(page.locator('[data-testid="control-checked"]')).toBeVisible();
await expect(page.locator('[data-testid="control-mode"]')).toBeVisible();
await expect(page.locator('[data-testid="control-state"]')).toBeVisible();
await expect(
page.locator('[data-testid="control-disabled"]'),
).toBeVisible();
await expect(page.locator('[data-testid="control-label"]')).toBeVisible();
});
test("should update when controls are changed", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Toggle checked control
await page.click('[data-testid="control-checked"]');
// Check that the checkbox is now checked
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
// Toggle back
await page.click('[data-testid="control-checked"]');
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("should change mode when mode control is changed", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Change mode to inverse
await page.selectOption('[data-testid="control-mode"]', "inverse");
// Check that the checkbox styling has changed (inverse mode)
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
// Change back to standard
await page.selectOption('[data-testid="control-mode"]', "standard");
await expect(checkbox).toBeVisible();
});
test("should show disabled state when disabled control is toggled", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Toggle disabled control
await page.click('[data-testid="control-disabled"]');
// Check that the checkbox is now disabled
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("aria-disabled", "true");
await expect(checkbox).toHaveAttribute("tabIndex", "-1");
// Toggle back
await page.click('[data-testid="control-disabled"]');
await expect(checkbox).toHaveAttribute("aria-disabled", "false");
await expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should update label when label control is changed", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Change label
await page.fill('[data-testid="control-label"]', "Custom Label");
// Check that the label has updated
await expect(page.locator("text=Custom Label")).toBeVisible();
});
test("should have proper accessibility in Storybook", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check accessibility attributes
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("role", "checkbox");
await expect(checkbox).toHaveAttribute("aria-checked");
await expect(checkbox).toHaveAttribute("tabIndex");
});
test("should support keyboard navigation in Storybook", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
const checkbox = page.locator('[role="checkbox"]').first();
// Focus the checkbox
await checkbox.focus();
await expect(checkbox).toBeFocused();
// Test keyboard activation
await checkbox.press(" ");
await expect(checkbox).toHaveAttribute("aria-checked", "true");
await checkbox.press(" ");
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("should show proper documentation", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Check that documentation is available
await expect(page.locator('[data-testid="docs-tab"]')).toBeVisible();
// Click on docs tab
await page.click('[data-testid="docs-tab"]');
// Check that documentation content is shown
await expect(page.locator("text=Checkbox")).toBeVisible();
await expect(page.locator("text=Props")).toBeVisible();
});
test("should have proper story navigation", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Test navigation between stories
const stories = ["Default", "Checked", "Standard", "Inverse"];
for (const story of stories) {
await page.click(`[data-testid="${story}"]`);
await expect(page.locator('[role="checkbox"]').first()).toBeVisible();
}
});
test("should maintain state between story switches", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Interact with checkbox
const checkbox = page.locator('[role="checkbox"]').first();
await checkbox.click();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
// Switch to another story and back
await page.click('[data-testid="Checked"]');
await page.click('[data-testid="Default"]');
// Check that the state is maintained
await expect(checkbox).toHaveAttribute("aria-checked", "true");
});
});
@@ -0,0 +1,124 @@
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should be unchecked initially
await expect(radioButton).toHaveAttribute("aria-checked", "false");
// Click to check
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Radio buttons can't be unchecked by clicking them again
// They stay checked until another radio button in the same group is selected
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
export const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should be checked initially
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Radio buttons can't be unchecked by clicking them again
// They stay checked until another radio button in the same group is selected
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// First should be unchecked
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
// Click first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// First should be unchecked
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
// Click first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Focus the radio button
await userEvent.click(radioButton);
await expect(radioButton).toHaveFocus();
// Test Space key
await userEvent.keyboard(" ");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Test Enter key
await userEvent.keyboard("Enter");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
},
};
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should have proper ARIA attributes
await expect(radioButton).toHaveAttribute("role", "radio");
await expect(radioButton).toHaveAttribute("aria-checked");
await expect(radioButton).toHaveAttribute("tabIndex", "0");
// Should be keyboard accessible
await userEvent.tab();
await expect(radioButton).toHaveFocus();
// Should have accessible name
const label = canvas.getByText("Default radio button");
await expect(label).toBeVisible();
},
};
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should have hidden input for form submission
const hiddenInput = canvas.getByRole("radio", { hidden: true });
await expect(hiddenInput).toBeInTheDocument();
// Should be included in form data
await userEvent.click(radioButton);
await expect(hiddenInput).toBeChecked();
},
};
@@ -0,0 +1,177 @@
import { test, expect } from "@playwright/test";
test.describe("RadioButton Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--default",
);
});
test("renders default story", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toBeVisible();
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("renders checked story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--checked",
);
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toBeVisible();
await expect(radioButton).toHaveAttribute("aria-checked", "true");
});
test("renders standard story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--standard",
);
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
// First should be unchecked
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("renders inverse story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--inverse",
);
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
// First should be unchecked
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("interacts with controls", async ({ page }) => {
// Test checked control
await page.check('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveAttribute("aria-checked", "true");
await page.uncheck('[data-testid="checked-control"]');
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("interacts with mode control", async ({ page }) => {
// Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
await page.selectOption('[data-testid="mode-control"]', "standard");
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
});
test("interacts with state control", async ({ page }) => {
// Test state control
await page.selectOption('[data-testid="state-control"]', "focus");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(/focus:outline/);
await page.selectOption('[data-testid="state-control"]', "hover");
await expect(radioButton).toHaveClass(/hover:outline/);
});
test("interacts with label control", async ({ page }) => {
// Test label control
await page.fill('[data-testid="label-control"]', "Custom Label");
await expect(page.locator('text="Custom Label"')).toBeVisible();
});
test("handles keyboard interaction", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await radioButton.focus();
await expect(radioButton).toBeFocused();
// Test Space key
await page.keyboard.press("Space");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Test Enter key
await page.keyboard.press("Enter");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("has proper accessibility attributes", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveAttribute("role", "radio");
await expect(radioButton).toHaveAttribute("aria-checked");
await expect(radioButton).toHaveAttribute("tabIndex", "0");
});
test("shows dot indicator when checked", async ({ page }) => {
await page.check('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
const dot = radioButton.locator("div").first();
await expect(dot).toHaveClass(/w-\[16px\]/, /h-\[16px\]/, /rounded-full/);
});
test("hides dot indicator when unchecked", async ({ page }) => {
await page.uncheck('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
const dot = radioButton.locator("div").first();
await expect(dot).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
});
test("maintains focus state", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await radioButton.focus();
await expect(radioButton).toBeFocused();
// Should maintain focus after interaction
await page.keyboard.press("Space");
await expect(radioButton).toBeFocused();
});
test("handles mouse interaction", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
// Click to check
await radioButton.click();
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Click to uncheck
await radioButton.click();
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("shows proper styling for different modes", async ({ page }) => {
// Test standard mode
await page.selectOption('[data-testid="mode-control"]', "standard");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
// Test inverse mode
await page.selectOption('[data-testid="mode-control"]', "inverse");
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
});
test("handles form submission", async ({ page }) => {
const hiddenInput = page.locator('input[type="radio"]');
await expect(hiddenInput).toBeVisible();
// Should be included in form data
await page.check('[data-testid="checked-control"]');
await expect(hiddenInput).toBeChecked();
});
});
@@ -0,0 +1,184 @@
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Should have 3 radio buttons
await expect(radioButtons).toHaveLength(3);
// First should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Second should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click first option
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// First should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click second option
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const InteractiveInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Should show initial state
await expect(canvas.getByText("Selected: option1")).toBeVisible();
// Click second option
await userEvent.click(radioButtons[1]);
await expect(canvas.getByText("Selected: option2")).toBeVisible();
// Click third option
await userEvent.click(radioButtons[2]);
await expect(canvas.getByText("Selected: option3")).toBeVisible();
},
};
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Focus first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await userEvent.tab();
await expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await userEvent.keyboard(" ");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Navigate to third radio button
await userEvent.tab();
await expect(radioButtons[2]).toHaveFocus();
// Activate with Enter
await userEvent.keyboard("Enter");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have proper ARIA attributes
await expect(radioGroup).toHaveAttribute("role", "radiogroup");
radioButtons.forEach(async (button) => {
await expect(button).toHaveAttribute("role", "radio");
await expect(button).toHaveAttribute("aria-checked");
await expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have accessible names
await expect(canvas.getByText("Option 1")).toBeVisible();
await expect(canvas.getByText("Option 2")).toBeVisible();
await expect(canvas.getByText("Option 3")).toBeVisible();
},
};
export const SingleSelectionInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Initially first should be selected
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click second option
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click third option
await userEvent.click(radioButtons[2]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
},
};
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have hidden inputs for form submission
const hiddenInputs = canvas.getAllByRole("radio", { hidden: true });
await expect(hiddenInputs).toHaveLength(3);
// All should have the same name
const names = await Promise.all(
hiddenInputs.map((input) => input.getAttribute("name")),
);
expect(names.every((name) => name === names[0])).toBe(true);
// Should be included in form data
await userEvent.click(radioButtons[1]);
await expect(hiddenInputs[1]).toBeChecked();
},
};
@@ -0,0 +1,253 @@
import { test, expect } from "@playwright/test";
test.describe("RadioGroup Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--default",
);
});
test("renders default story", async ({ page }) => {
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
});
test("renders standard story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--standard",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// Second option should be selected
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("renders inverse story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--inverse",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// First option should be selected
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "true");
});
test("renders interactive story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--interactive",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// Should show selected value
await expect(page.locator('text="Selected: option1"')).toBeVisible();
});
test("interacts with controls", async ({ page }) => {
// Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioGroup = page.locator('[role="radiogroup"]');
const radioButtons = page.locator('[role="radio"]');
// All radio buttons should have inverse styling
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
}
await page.selectOption('[data-testid="mode-control"]', "standard");
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
}
});
test("interacts with value control", async ({ page }) => {
// Test value control
await page.fill('[data-testid="value-control"]', "option2");
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
});
test("handles keyboard navigation", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus first radio button
await radioButtons.first().focus();
await expect(radioButtons.first()).toBeFocused();
// Navigate to second radio button
await page.keyboard.press("Tab");
await expect(radioButtons.nth(1)).toBeFocused();
// Navigate to third radio button
await page.keyboard.press("Tab");
await expect(radioButtons.nth(2)).toBeFocused();
});
test("handles keyboard activation", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus second radio button
await radioButtons.nth(1).focus();
// Activate with Space
await page.keyboard.press("Space");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Activate third radio button with Enter
await radioButtons.nth(2).focus();
await page.keyboard.press("Enter");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
});
test("handles mouse interaction", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Click second option
await radioButtons.nth(1).click();
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Click third option
await radioButtons.nth(2).click();
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
});
test("maintains single selection", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Click first option
await radioButtons.first().click();
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
// Click second option
await radioButtons.nth(1).click();
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
});
test("has proper accessibility attributes", async ({ page }) => {
const radioGroup = page.locator('[role="radiogroup"]');
const radioButtons = page.locator('[role="radio"]');
await expect(radioGroup).toHaveAttribute("role", "radiogroup");
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveAttribute("role", "radio");
await expect(radioButtons.nth(i)).toHaveAttribute("aria-checked");
await expect(radioButtons.nth(i)).toHaveAttribute("tabIndex", "0");
}
});
test("shows proper labels", async ({ page }) => {
await expect(page.locator('text="Option 1"')).toBeVisible();
await expect(page.locator('text="Option 2"')).toBeVisible();
await expect(page.locator('text="Option 3"')).toBeVisible();
});
test("handles form submission", async ({ page }) => {
const hiddenInputs = page.locator('input[type="radio"]');
await expect(hiddenInputs).toHaveCount(3);
// All should have the same name
const names = await hiddenInputs.evaluateAll((inputs) =>
inputs.map((input) => input.getAttribute("name")),
);
expect(names.every((name) => name === names[0])).toBe(true);
});
test("shows dot indicators correctly", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Initially first option should be selected
const firstDot = radioButtons.first().locator("div").first();
await expect(firstDot).toHaveClass(
/w-\[16px\]/,
/h-\[16px\]/,
/rounded-full/,
);
// Click second option
await radioButtons.nth(1).click();
// First dot should be hidden, second should be visible
const secondDot = radioButtons.nth(1).locator("div").first();
await expect(secondDot).toHaveClass(
/w-\[16px\]/,
/h-\[16px\]/,
/rounded-full/,
);
});
test("handles interactive story state changes", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--interactive",
);
// Should show initial state
await expect(page.locator('text="Selected: option1"')).toBeVisible();
// Click second option
const radioButtons = page.locator('[role="radio"]');
await radioButtons.nth(1).click();
// Should update displayed value
await expect(page.locator('text="Selected: option2"')).toBeVisible();
});
test("maintains focus state", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus first radio button
await radioButtons.first().focus();
await expect(radioButtons.first()).toBeFocused();
// Should maintain focus after interaction
await page.keyboard.press("Space");
await expect(radioButtons.first()).toBeFocused();
});
test("handles different viewport sizes", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await expect(radioGroup).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await expect(radioGroup).toBeVisible();
});
});
+166
View File
@@ -0,0 +1,166 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Checkbox from "../../app/components/Checkbox";
describe("Checkbox Component", () => {
test("renders with default props", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("renders with label", () => {
render(<Checkbox label="Test checkbox" />);
expect(screen.getByText("Test checkbox")).toBeInTheDocument();
});
test("renders as checked when checked prop is true", () => {
render(<Checkbox checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
});
test("renders as unchecked when checked prop is false", () => {
render(<Checkbox checked={false} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
event: expect.any(Object),
});
});
test("calls onChange when toggled from checked to unchecked", () => {
const handleChange = vi.fn();
render(<Checkbox checked={true} onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalledWith({
checked: false,
value: undefined,
event: expect.any(Object),
});
});
test("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
// Test Space key
fireEvent.keyDown(checkbox, { key: " " });
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
event: expect.any(Object),
});
// Test Enter key
fireEvent.keyDown(checkbox, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(<Checkbox disabled={true} onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).not.toHaveBeenCalled();
});
test("applies disabled attributes when disabled", () => {
render(<Checkbox disabled={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-disabled", "true");
expect(checkbox).toHaveAttribute("tabIndex", "-1");
});
test("applies correct tabIndex when not disabled", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("renders with standard mode by default", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
});
test("renders with inverse mode", () => {
render(<Checkbox mode="inverse" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
});
test("applies custom className", () => {
render(<Checkbox className="custom-class" />);
const label = screen.getByRole("checkbox").closest("label");
expect(label).toHaveClass("custom-class");
});
test("passes through additional props", () => {
render(<Checkbox id="test-checkbox" name="test" value="test-value" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("id", "test-checkbox");
});
test("renders hidden native input for form compatibility", () => {
render(<Checkbox name="test" value="test-value" checked={true} />);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "checkbox");
expect(hiddenInput).toHaveAttribute("name", "test");
expect(hiddenInput).toBeChecked();
});
test("applies aria-label when provided", () => {
render(<Checkbox ariaLabel="Custom label" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-label", "Custom label");
});
test("prevents default on mouse down", () => {
render(<Checkbox />);
const label = screen.getByRole("checkbox").closest("label");
const mouseDownEvent = new MouseEvent("mousedown", { bubbles: true });
const preventDefaultSpy = vi.spyOn(mouseDownEvent, "preventDefault");
fireEvent(label, mouseDownEvent);
expect(preventDefaultSpy).toHaveBeenCalled();
});
test("renders checkmark SVG when checked", () => {
render(<Checkbox checked={true} />);
const svg = screen.getByRole("checkbox").querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("aria-hidden", "true");
expect(svg).toHaveAttribute("focusable", "false");
});
test("does not render checkmark SVG when unchecked", () => {
render(<Checkbox checked={false} />);
const svg = screen.getByRole("checkbox").querySelector("svg");
expect(svg).toBeInTheDocument();
// SVG should be present but checkmark should be transparent
const path = svg.querySelector("polyline");
expect(path).toHaveAttribute("stroke", "transparent");
});
});
+321
View File
@@ -0,0 +1,321 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi, beforeEach } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
expect.extend(toHaveNoViolations);
describe("ContextMenu Component", () => {
const defaultProps = {
children: "Context Menu Content",
};
describe("Rendering", () => {
it("renders with default props", () => {
render(<ContextMenu {...defaultProps} />);
expect(screen.getByText("Context Menu Content")).toBeInTheDocument();
});
it("renders with custom className", () => {
render(<ContextMenu {...defaultProps} className="custom-class" />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveClass("custom-class");
});
it("applies correct base styles", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveClass(
"bg-black",
"border",
"rounded-[var(--measures-radius-medium)]",
"shadow-lg",
"p-[4px]",
);
});
it("has solid black background", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveStyle({ backgroundColor: "#000000" });
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu {...defaultProps}>
<ContextMenuItem onClick={vi.fn()}>Menu Item</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveAttribute("role", "menu");
});
});
});
describe("ContextMenuItem Component", () => {
const defaultProps = {
children: "Menu Item",
onClick: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("Rendering", () => {
it("renders with default props", () => {
render(<ContextMenuItem {...defaultProps} />);
expect(screen.getByText("Menu Item")).toBeInTheDocument();
});
it("renders as selected when selected prop is true", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
"rounded-[var(--measures-radius-small)]",
);
});
it("renders with submenu arrow when hasSubmenu prop is true", () => {
render(<ContextMenuItem {...defaultProps} hasSubmenu={true} />);
// Check for the right-pointing chevron SVG
const item = screen.getByRole("menuitem");
const svg = item.querySelector("svg:last-child");
expect(svg).toBeInTheDocument();
});
it("renders with checkmark when selected prop is true", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
// Check for the checkmark SVG
const item = screen.getByRole("menuitem");
const svg = item.querySelector("svg:first-child");
expect(svg).toBeInTheDocument();
});
it("applies correct size styles", () => {
render(<ContextMenuItem {...defaultProps} size="small" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[10px]", "leading-[14px]");
});
it("applies medium size styles", () => {
render(<ContextMenuItem {...defaultProps} size="medium" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[14px]", "leading-[20px]");
});
it("applies large size styles", () => {
render(<ContextMenuItem {...defaultProps} size="large" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[16px]", "leading-[24px]");
});
});
describe("Interaction", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByText("Menu Item");
await user.click(item);
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
});
it("does not call onClick when disabled", async () => {
const user = userEvent.setup();
render(<ContextMenuItem {...defaultProps} disabled={true} />);
const item = screen.getByText("Menu Item");
await user.click(item);
expect(defaultProps.onClick).not.toHaveBeenCalled();
});
it("has hover effects", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem {...defaultProps} />
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toBeInTheDocument();
});
});
describe("Styling", () => {
it("applies correct text color", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"text-[var(--color-content-default-brand-primary)]",
);
});
it("applies correct padding", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("px-[8px]", "py-[4px]");
});
it("applies correct gap between checkmark and text", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
const item = screen.getByText("Menu Item").closest("div");
expect(item).toHaveClass("gap-[8px]");
});
});
});
describe("ContextMenuSection Component", () => {
const defaultProps = {
title: "Section Title",
children: "Section Content",
};
describe("Rendering", () => {
it("renders with title and children", () => {
render(<ContextMenuSection {...defaultProps} />);
expect(screen.getByText("Section Title")).toBeInTheDocument();
expect(screen.getByText("Section Content")).toBeInTheDocument();
});
it("renders without title when not provided", () => {
render(<ContextMenuSection>Section Content</ContextMenuSection>);
expect(screen.getByText("Section Content")).toBeInTheDocument();
expect(screen.queryByText("Section Title")).not.toBeInTheDocument();
});
it("applies correct title styling", () => {
render(<ContextMenuSection {...defaultProps} />);
const title = screen.getByText("Section Title");
expect(title).toHaveClass(
"text-[var(--color-content-default-primary)]",
"font-medium",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuSection {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
describe("ContextMenuDivider Component", () => {
describe("Rendering", () => {
it("renders divider", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toBeInTheDocument();
});
it("applies correct styling", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-t",
"border-[var(--color-border-default-tertiary)]",
"my-1",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuDivider />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
describe("ContextMenu Components Integration", () => {
const TestMenu = () => (
<ContextMenu>
<ContextMenuSection title="First Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Second Section">
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Item 3
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
it("renders all components together", () => {
render(<TestMenu />);
expect(screen.getByText("First Section")).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
expect(screen.getByText("Second Section")).toBeInTheDocument();
expect(screen.getByText("Item 3")).toBeInTheDocument();
expect(screen.getByRole("separator")).toBeInTheDocument();
});
it("has no accessibility violations when integrated", async () => {
const { container } = render(<TestMenu />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
+271
View File
@@ -0,0 +1,271 @@
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-[32px]");
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-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
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:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
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).toHaveClass("text-[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"));
});
});
+248
View File
@@ -0,0 +1,248 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../app/components/RadioButton";
describe("RadioButton", () => {
it("renders with default props", () => {
render(<RadioButton />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toBeInTheDocument();
expect(radioButton).toHaveAttribute("aria-checked", "false");
});
it("renders with label", () => {
render(<RadioButton label="Test Radio" />);
expect(screen.getByText("Test Radio")).toBeInTheDocument();
});
it("shows checked state", () => {
render(<RadioButton checked={true} label="Checked Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("calls onChange when clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("calls onChange with value when clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
value="test-value"
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: "test-value",
});
});
it("does not call onChange when clicking already checked radio button", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton checked={true} onChange={handleChange} label="Test Radio" />,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
// Radio buttons should not be unchecked by clicking them again
expect(handleChange).not.toHaveBeenCalled();
});
it("handles keyboard activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton); // Focus the element first
await user.keyboard("{Enter}");
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("applies standard mode classes", () => {
render(<RadioButton mode="standard" label="Standard Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
it("applies inverse mode classes", () => {
render(<RadioButton mode="inverse" label="Inverse Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
it("applies focus state classes", () => {
render(<RadioButton state="focus" label="Focus Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass("focus:outline");
});
it("applies hover state classes", () => {
render(<RadioButton state="hover" label="Hover Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass("hover:outline");
});
it("renders hidden input for form submission", () => {
render(
<RadioButton
name="test-radio"
value="test-value"
checked={true}
label="Test Radio"
/>,
);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "radio");
expect(hiddenInput).toHaveAttribute("name", "test-radio");
expect(hiddenInput).toBeChecked();
});
it("applies custom className", () => {
render(<RadioButton className="custom-class" label="Custom Radio" />);
const label = screen.getByText("Custom Radio").closest("label");
expect(label).toHaveClass("custom-class");
});
it("generates unique ID when not provided", () => {
render(<RadioButton label="Radio 1" />);
render(<RadioButton label="Radio 2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("id");
expect(radioButtons[1]).toHaveAttribute("id");
expect(radioButtons[0].id).not.toBe(radioButtons[1].id);
});
it("uses provided ID", () => {
render(<RadioButton id="custom-id" label="Custom ID Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("id", "custom-id");
});
it("associates label with radio button for accessibility", () => {
render(<RadioButton label="Accessible Radio" />);
const radioButton = screen.getByRole("radio");
const labelId = radioButton.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent("Accessible Radio");
});
it("uses aria-label when provided", () => {
render(<RadioButton ariaLabel="Custom Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
});
it("shows dot indicator when checked", () => {
render(
<RadioButton checked={true} mode="standard" label="Checked Radio" />,
);
const dot = screen.getByRole("radio").querySelector("div");
expect(dot).toHaveClass("w-[16px]", "h-[16px]", "rounded-full");
});
it("hides dot indicator when unchecked", () => {
render(
<RadioButton checked={false} mode="standard" label="Unchecked Radio" />,
);
const dot = screen.getByRole("radio").querySelector("div");
// Check if the dot has transparent background or no background color set
const computedStyle = window.getComputedStyle(dot);
const backgroundColor = computedStyle.backgroundColor;
// The dot should either be transparent or have no background color
expect(
backgroundColor === "transparent" ||
backgroundColor === "rgba(0, 0, 0, 0)" ||
backgroundColor === "",
).toBe(true);
});
});
+240
View File
@@ -0,0 +1,240 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../app/components/RadioGroup";
describe("RadioGroup", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("renders with default props", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons).toHaveLength(3);
});
it("renders all options", () => {
render(<RadioGroup options={defaultOptions} />);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
it("shows selected option", () => {
render(<RadioGroup options={defaultOptions} value="option2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
});
it("calls onChange when option is selected", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("updates selection when different option is clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
// Click option 3
const option3 = screen.getByText("Option 3").closest("label");
await user.click(option3);
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
await user.click(radioButtons[2]);
await user.keyboard("{Enter}");
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("applies standard mode to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} mode="standard" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
});
it("applies inverse mode to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} mode="inverse" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
});
it("applies state to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} state="focus" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass("focus:outline");
});
});
it("generates unique group name when not provided", () => {
render(<RadioGroup options={defaultOptions} />);
render(<RadioGroup options={defaultOptions} />);
const hiddenInputs = screen.getAllByRole("radio", { hidden: true });
const names = hiddenInputs.map((input) => input.getAttribute("name"));
// Should have unique names
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBeGreaterThan(1);
});
it("uses provided name for all radio buttons", () => {
render(<RadioGroup options={defaultOptions} name="test-group" />);
const hiddenInputs = screen.getAllByDisplayValue("option1");
hiddenInputs.forEach((input) => {
expect(input).toHaveAttribute("name", "test-group");
});
});
it("applies custom className to container", () => {
render(<RadioGroup options={defaultOptions} className="custom-group" />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveClass("custom-group");
});
it("passes aria-label to radiogroup", () => {
render(
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
});
it("handles empty options array", () => {
render(<RadioGroup options={[]} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.queryAllByRole("radio");
expect(radioButtons).toHaveLength(0);
});
it("handles options with ariaLabel", () => {
const optionsWithAria = [
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
];
render(<RadioGroup options={optionsWithAria} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
});
it("maintains selection state correctly", () => {
const { rerender } = render(
<RadioGroup options={defaultOptions} value="option1" />,
);
let radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
rerender(<RadioGroup options={defaultOptions} value="option3" />);
radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
});
it("does not call onChange when clicking already selected option", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option2"
onChange={handleChange}
/>,
);
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
// Should not call onChange since it's already selected
expect(handleChange).not.toHaveBeenCalled();
});
});
+402
View File
@@ -0,0 +1,402 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Select from "../../app/components/Select";
expect.extend(toHaveNoViolations);
describe("Select Component", () => {
const defaultProps = {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
};
describe("Rendering", () => {
it("renders with default props", () => {
render(<Select {...defaultProps} />);
expect(screen.getByText("Test Select")).toBeInTheDocument();
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("renders without label when not provided", () => {
render(
<Select
placeholder="Select an option"
options={defaultProps.options}
/>,
);
expect(screen.queryByText("Test Select")).not.toBeInTheDocument();
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("renders with horizontal label variant", () => {
render(<Select {...defaultProps} labelVariant="horizontal" />);
const container = screen.getByText("Test Select").closest("div");
expect(container).toHaveClass("flex", "items-center");
});
it("renders with default label variant", () => {
render(<Select {...defaultProps} labelVariant="default" />);
const container = screen.getByText("Test Select").closest("div");
expect(container).toHaveClass("flex", "flex-col");
});
});
describe("Size Variants", () => {
it("renders small size correctly", () => {
render(<Select {...defaultProps} size="small" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[32px]");
});
it("renders medium size correctly", () => {
render(<Select {...defaultProps} size="medium" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[36px]");
});
it("renders large size correctly", () => {
render(<Select {...defaultProps} size="large" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[40px]");
});
it("applies correct height for small horizontal label", () => {
render(
<Select {...defaultProps} size="small" labelVariant="horizontal" />,
);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[30px]");
});
it("applies correct height for small default label", () => {
render(<Select {...defaultProps} size="small" labelVariant="default" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[32px]");
});
});
describe("State Variants", () => {
it("renders default state", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
it("renders hover state", () => {
render(<Select {...defaultProps} state="hover" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
it("renders focus state", () => {
render(<Select {...defaultProps} state="focus" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("renders error state", () => {
render(<Select {...defaultProps} error={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
it("renders disabled state", () => {
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("cursor-not-allowed");
expect(selectButton).toHaveClass("opacity-40");
});
});
describe("Interaction", () => {
it("opens dropdown when clicked", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
});
it("closes dropdown when clicked again", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(selectButton);
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
it("selects an option when clicked", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<Select {...defaultProps} onChange={onChange} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(onChange).toHaveBeenCalledWith({
target: {
value: "option1",
text: "Option 1",
},
});
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
it("closes dropdown when option is selected", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
await waitFor(() => {
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
});
});
it("does not open when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
describe("Keyboard Navigation", () => {
it("opens dropdown with Enter key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("opens dropdown with Space key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard(" ");
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("closes dropdown with Escape key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
it("does not respond to keyboard when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
describe("Click Outside", () => {
it("closes dropdown when clicking outside", async () => {
const user = userEvent.setup();
render(
<div>
<Select {...defaultProps} />
<div data-testid="outside">Outside element</div>
</div>,
);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByTestId("outside"));
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
});
describe("Value Display", () => {
it("shows placeholder when no value selected", () => {
render(<Select {...defaultProps} />);
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("shows selected value when option is selected", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.queryByText("Select an option")).not.toBeInTheDocument();
});
it("shows selected value when value prop is provided", () => {
render(<Select {...defaultProps} value="option2" />);
expect(screen.getByText("Option 2")).toBeInTheDocument();
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<Select {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper ARIA attributes", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAttribute("aria-expanded", "false");
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
});
it("updates aria-expanded when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveAttribute("aria-expanded", "true");
});
});
it("associates label with select button", () => {
render(<Select {...defaultProps} />);
const label = screen.getByText("Test Select");
const selectButton = screen.getByRole("button");
expect(label).toHaveAttribute("for", selectButton.id);
});
});
describe("Focus Behavior", () => {
it("enters focus state when tabbed to", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.tab();
expect(selectButton).toHaveFocus();
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
it("does not enter focus state when clicked", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
expect(selectButton).toHaveFocus();
// Focus state should not be applied on click, only on keyboard navigation
});
});
});
+184
View File
@@ -0,0 +1,184 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Switch from "../../app/components/Switch";
describe("Switch Component", () => {
it("renders with default props", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toBeInTheDocument();
expect(switchButton).toHaveAttribute("aria-checked", "false");
});
it("renders with custom props", () => {
const handleChange = vi.fn();
render(
<Switch
checked={true}
onChange={handleChange}
label="Test Switch"
state="focus"
/>,
);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
expect(screen.getByText("Test Switch")).toBeInTheDocument();
});
it("handles checked prop correctly", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles state prop correctly", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
fireEvent.click(switchButton);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Switch onBlur={handleBlur} />);
const switchButton = screen.getByRole("switch");
fireEvent.blur(switchButton);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(switchButton, { key: "Tab" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
});
it("applies correct track styles based on checked state", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
let track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
});
it("applies correct focus styles", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("applies correct base classes", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass(
"relative",
"inline-flex",
"items-center",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<Switch ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<Switch className="custom-class" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("custom-class");
});
it("renders label when provided", () => {
render(<Switch label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
});
it("does not render label when not provided", () => {
render(<Switch />);
expect(screen.queryByText("Switch label")).not.toBeInTheDocument();
// Should have aria-label for accessibility
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-label", "Toggle switch");
});
it("applies correct label styles", () => {
render(<Switch label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass(
"ml-[var(--measures-spacing-008)]",
"font-inter",
"font-normal",
"text-[14px]",
"leading-[20px]",
"text-[var(--color-content-default-primary)]",
);
});
});
+203
View File
@@ -0,0 +1,203 @@
import { expect, test, describe, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
describe("TextArea", () => {
test("renders with default props", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("renders with label", () => {
render(<TextArea label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByLabelText("Test Label")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<TextArea placeholder="Enter text..." />);
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument();
});
test("renders with value", () => {
render(<TextArea value="Test value" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveValue("Test value");
});
test("renders with different sizes", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("renders with horizontal label variant", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("renders with default label variant", () => {
render(<TextArea labelVariant="default" label="Default Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
});
test("applies disabled state", () => {
render(<TextArea disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("applies error state", () => {
render(<TextArea error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("applies different states", () => {
const { rerender } = render(<TextArea state="active" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<TextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
rerender(<TextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]",
);
});
test("calls onChange when text changes", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).toHaveBeenCalledTimes(4);
});
test("calls onFocus when focused", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
render(<TextArea onFocus={handleFocus} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
});
test("calls onBlur when blurred", async () => {
const user = userEvent.setup();
const handleBlur = vi.fn();
render(<TextArea onBlur={handleBlur} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("does not call onChange when disabled", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea disabled onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).not.toHaveBeenCalled();
});
test("applies custom className", () => {
render(<TextArea className="custom-class" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("custom-class");
});
test("forwards ref", () => {
const ref = vi.fn();
render(<TextArea ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies correct height for small horizontal label", () => {
render(
<TextArea
size="small"
labelVariant="horizontal"
label="Small Horizontal"
/>,
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
});
test("applies correct height for medium horizontal label", () => {
render(
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
/>,
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[110px]");
});
test("applies correct border radius for different sizes", () => {
const { rerender } = render(<TextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-small)",
});
});
test("applies correct text color", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
});
test("applies correct label color", () => {
render(<TextArea label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
});
+195
View File
@@ -0,0 +1,195 @@
import { expect, test, describe, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Toggle from "../../app/components/Toggle";
describe("Toggle Component", () => {
test("renders with default props", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
const label = screen.getByText("Test Toggle");
expect(toggle).toBeInTheDocument();
expect(label).toBeInTheDocument();
expect(toggle).toHaveAttribute("type", "button");
});
test("renders with custom props", () => {
render(
<Toggle
label="Custom Toggle"
checked={true}
disabled={true}
className="custom-class"
/>,
);
const toggle = screen.getByRole("switch");
expect(toggle).toBeInTheDocument();
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle).toHaveAttribute("disabled");
});
test("handles checked state", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
});
test("handles disabled state", () => {
render(<Toggle label="Test Toggle" disabled={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
expect(toggle).toHaveClass("cursor-not-allowed");
});
test("handles state prop", () => {
const { rerender } = render(<Toggle label="Test Toggle" state="focus" />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Toggle label="Test Toggle" state="default" />);
toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
test("handles showIcon and icon props", () => {
render(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
});
test("handles showText and text props", () => {
render(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("Toggle");
});
test("handles both icon and text", () => {
render(
<Toggle
label="Test Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>,
);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
expect(toggle).toHaveTextContent("Toggle");
});
test("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(handleChange).toHaveBeenCalledTimes(1);
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(handleChange).not.toHaveBeenCalled();
});
test("applies correct classes for different states", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-surface-default-primary)]");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<Toggle label="Test Toggle" disabled={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
});
test("applies hover classes when not checked", () => {
render(<Toggle label="Test Toggle" checked={false} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
test("does not apply hover classes when checked", () => {
render(<Toggle label="Test Toggle" checked={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
test("applies focus-visible classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
});
test("applies correct size classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("h-[var(--measures-sizing-032)]");
expect(toggle).toHaveClass("px-[16px]");
expect(toggle).toHaveClass("py-[8px]");
expect(toggle).toHaveClass("gap-[4px]");
});
test("applies correct text classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("text-[12px]");
expect(toggle).toHaveClass("leading-[16px]");
});
test("applies correct label classes", () => {
render(<Toggle label="Test Toggle" />);
const label = screen.getByText("Test Toggle");
expect(label).toHaveClass("text-[12px]");
expect(label).toHaveClass("leading-[16px]");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("forwards ref correctly", () => {
const ref = vi.fn();
render(<Toggle label="Test Toggle" ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies custom className", () => {
render(<Toggle label="Test Toggle" className="custom-class" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("custom-class");
});
});
+213
View File
@@ -0,0 +1,213 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ToggleGroup from "../../app/components/ToggleGroup";
describe("ToggleGroup Component", () => {
it("renders with default props", () => {
render(<ToggleGroup>Test Content</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toBeInTheDocument();
expect(toggleGroup).toHaveTextContent("Test Content");
});
it("renders with custom props", () => {
render(
<ToggleGroup position="middle" state="selected" showText={true}>
Custom Content
</ToggleGroup>,
);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toBeInTheDocument();
expect(toggleGroup).toHaveTextContent("Custom Content");
});
it("handles position prop correctly", () => {
const { rerender } = render(
<ToggleGroup position="left">Left</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-r-[var(--measures-radius-medium)]",
"rounded-l-none",
);
});
it("handles state prop correctly", () => {
const { rerender } = render(
<ToggleGroup state="default">Default</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
);
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
"shadow-[0_0_5px_1px_#3281F8]",
);
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("handles showText prop correctly", () => {
const { rerender } = render(
<ToggleGroup showText={true}>Visible Text</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Visible Text");
rerender(<ToggleGroup showText={false}></ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("☰");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Clickable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.click(toggleGroup);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<ToggleGroup onFocus={handleFocus}>Focusable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.focus(toggleGroup);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<ToggleGroup onBlur={handleBlur}>Blurable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.blur(toggleGroup);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Keyboard</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
// Test Enter key
fireEvent.keyDown(toggleGroup, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(toggleGroup, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(toggleGroup, { key: "Escape" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(
<ToggleGroup state="default">Default</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
);
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("applies correct position classes", () => {
const { rerender } = render(
<ToggleGroup position="left">Left</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-r-[var(--measures-radius-medium)]",
"rounded-l-none",
);
});
it("applies correct base classes", () => {
render(<ToggleGroup>Base</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"py-[var(--measures-spacing-008)]",
"px-[var(--measures-spacing-008)]",
"gap-[var(--measures-spacing-008)]",
"font-inter",
"font-medium",
"text-[12px]",
"leading-[12px]",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_1px_#3281F8]",
"hover:bg-[var(--color-magenta-magenta100)]",
"flex",
"items-center",
"justify-center",
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<ToggleGroup ref={ref}>Ref Test</ToggleGroup>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<ToggleGroup className="custom-class">Custom</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("custom-class");
});
});
+83
View File
@@ -0,0 +1,83 @@
import { test, expect } from "@playwright/test";
test.describe("Checkbox Visual Regression Tests", () => {
test("Standard mode - unchecked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-unchecked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-unchecked.png");
});
test("Standard mode - checked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-checked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-checked.png");
});
test("Inverse mode - unchecked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="inverse-unchecked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-unchecked.png");
});
test("Inverse mode - checked", async ({ page }) => {
await page.goto("/forms");
await expect(page.locator('[data-testid="inverse-checked"]')).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-checked.png");
});
test("Standard mode - hover state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="standard-unchecked"]');
await checkbox.hover();
await expect(page).toHaveScreenshot("checkbox-standard-hover.png");
});
test("Standard mode - focus state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="standard-unchecked"]');
await checkbox.focus();
await expect(page).toHaveScreenshot("checkbox-standard-focus.png");
});
test("Inverse mode - hover state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="inverse-unchecked"]');
await checkbox.hover();
await expect(page).toHaveScreenshot("checkbox-inverse-hover.png");
});
test("Inverse mode - focus state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="inverse-unchecked"]');
await checkbox.focus();
await expect(page).toHaveScreenshot("checkbox-inverse-focus.png");
});
test("Disabled state - standard", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-disabled"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-disabled.png");
});
test("Disabled state - inverse", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="inverse-disabled"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-disabled.png");
});
test("All variations grid", async ({ page }) => {
await page.goto("/forms");
await expect(page.locator('[data-testid="checkbox-grid"]')).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-all-variations.png");
});
});
+5 -1
View File
@@ -16,7 +16,11 @@ export default defineConfig({
"tests/unit/**/*.test.{js,jsx,ts,tsx}",
"tests/integration/**/*.test.{js,jsx,ts,tsx}",
"tests/accessibility/**/*.test.{js,jsx,ts,tsx}",
"tests/e2e/**/*.test.{js,jsx,ts,tsx}",
"tests/e2e/**/*.e2e.test.{js,jsx,ts,tsx}",
],
exclude: [
"tests/e2e/**/*.storybook.test.{js,jsx,ts,tsx}",
"tests/e2e/**/*.spec.{js,jsx,ts,tsx}",
],
css: true,
coverage: {