Form Components #22
@@ -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
@@ -10,7 +10,7 @@ const config = {
|
||||
"@storybook/addon-a11y",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/nextjs-vite",
|
||||
name: "@storybook/nextjs",
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
Generated
+5251
-850
File diff suppressed because it is too large
Load Diff
+10
-13
@@ -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",
|
||||
|
||||
@@ -4,6 +4,7 @@ export default defineConfig({
|
||||
testDir: "tests",
|
||||
testMatch: [
|
||||
"tests/e2e/**/*.spec.{js,ts}",
|
||||
"tests/e2e/**/*.test.{js,ts}",
|
||||
"tests/accessibility/**/*.spec.{js,ts}",
|
||||
],
|
||||
timeout: 60_000,
|
||||
|
||||
@@ -0,0 +1,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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1,269 @@
|
||||
import React from "react";
|
||||
import Input from "../app/components/Input";
|
||||
|
||||
export default {
|
||||
title: "Forms/Input",
|
||||
component: Input,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: { type: "select" },
|
||||
options: ["small", "medium", "large"],
|
||||
},
|
||||
labelVariant: {
|
||||
control: { type: "select" },
|
||||
options: ["default", "horizontal"],
|
||||
},
|
||||
state: {
|
||||
control: { type: "select" },
|
||||
options: ["default", "active", "hover", "focus", "error", "disabled"],
|
||||
},
|
||||
disabled: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
error: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
placeholder: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
value: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => <Input {...args} />;
|
||||
|
||||
// Default story
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
label: "Default Input",
|
||||
placeholder: "Enter text...",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
// Size variants
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
label: "Small Input",
|
||||
placeholder: "Small size",
|
||||
size: "small",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
export const Medium = Template.bind({});
|
||||
Medium.args = {
|
||||
label: "Medium Input",
|
||||
placeholder: "Medium size",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
label: "Large Input",
|
||||
placeholder: "Large size",
|
||||
size: "large",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
// Label variants
|
||||
export const DefaultLabel = Template.bind({});
|
||||
DefaultLabel.args = {
|
||||
label: "Default Label (Top)",
|
||||
placeholder: "Top label",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
export const HorizontalLabel = Template.bind({});
|
||||
HorizontalLabel.args = {
|
||||
label: "Horizontal Label",
|
||||
placeholder: "Left label",
|
||||
size: "medium",
|
||||
labelVariant: "horizontal",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
// States
|
||||
export const Active = Template.bind({});
|
||||
Active.args = {
|
||||
label: "Active State",
|
||||
placeholder: "Active input",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "active",
|
||||
};
|
||||
|
||||
export const Hover = Template.bind({});
|
||||
Hover.args = {
|
||||
label: "Hover State",
|
||||
placeholder: "Hover input",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "hover",
|
||||
};
|
||||
|
||||
export const Focus = Template.bind({});
|
||||
Focus.args = {
|
||||
label: "Focus State",
|
||||
placeholder: "Focused input",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "focus",
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
Error.args = {
|
||||
label: "Error State",
|
||||
placeholder: "Error input",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
error: true,
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
label: "Disabled State",
|
||||
placeholder: "Disabled input",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
// Interactive example
|
||||
export const Interactive = (args) => {
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<p className="text-sm text-gray-600">Current value: "{value}"</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Interactive.args = {
|
||||
label: "Interactive Input",
|
||||
placeholder: "Type something...",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
state: "default",
|
||||
};
|
||||
|
||||
// All sizes comparison
|
||||
export const AllSizes = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Small Size</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Small Default"
|
||||
placeholder="Small with top label"
|
||||
size="small"
|
||||
labelVariant="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Medium Size</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Medium Default"
|
||||
placeholder="Medium with top label"
|
||||
size="medium"
|
||||
labelVariant="default"
|
||||
/>
|
||||
<Input
|
||||
label="Medium Horizontal"
|
||||
placeholder="Medium with left label"
|
||||
size="medium"
|
||||
labelVariant="horizontal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Large Size</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Large Default"
|
||||
placeholder="Large with top label"
|
||||
size="large"
|
||||
labelVariant="default"
|
||||
/>
|
||||
<Input
|
||||
label="Large Horizontal"
|
||||
placeholder="Large with left label"
|
||||
size="large"
|
||||
labelVariant="horizontal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// All states comparison
|
||||
export const AllStates = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Input States</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Default State"
|
||||
placeholder="Default input"
|
||||
size="medium"
|
||||
state="default"
|
||||
/>
|
||||
<Input
|
||||
label="Active State"
|
||||
placeholder="Active input"
|
||||
size="medium"
|
||||
state="active"
|
||||
/>
|
||||
<Input
|
||||
label="Hover State"
|
||||
placeholder="Hover input"
|
||||
size="medium"
|
||||
state="hover"
|
||||
/>
|
||||
<Input
|
||||
label="Focus State"
|
||||
placeholder="Focused input"
|
||||
size="medium"
|
||||
state="focus"
|
||||
/>
|
||||
<Input
|
||||
label="Error State"
|
||||
placeholder="Error input"
|
||||
size="medium"
|
||||
error={true}
|
||||
/>
|
||||
<Input
|
||||
label="Disabled State"
|
||||
placeholder="Disabled input"
|
||||
size="medium"
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Input from "../../app/components/Input";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Input Component Accessibility", () => {
|
||||
test("has no accessibility violations", async () => {
|
||||
const { container } = render(<Input label="Test input" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations when disabled", async () => {
|
||||
const { container } = render(<Input label="Test input" disabled={true} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations when in error state", async () => {
|
||||
const { container } = render(<Input label="Test input" error={true} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations with horizontal label", async () => {
|
||||
const { container } = render(
|
||||
<Input label="Test input" labelVariant="horizontal" />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("associates label with input correctly", () => {
|
||||
render(<Input label="Test input" />);
|
||||
const input = screen.getByLabelText("Test input");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute("type", "text");
|
||||
});
|
||||
|
||||
test("maintains label association with custom ID", () => {
|
||||
render(<Input id="custom-input" label="Test input" />);
|
||||
const input = screen.getByLabelText("Test input");
|
||||
expect(input).toHaveAttribute("id", "custom-input");
|
||||
});
|
||||
|
||||
test("supports keyboard navigation", () => {
|
||||
render(<Input label="Test input" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Input should be focusable
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports keyboard activation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input label="Test input" onChange={handleChange} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Type in the input
|
||||
fireEvent.change(input, { target: { value: "test" } });
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("supports Enter key activation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input label="Test input" onChange={handleChange} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Focus the input first
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
// Input should still be focused and ready for typing
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports Space key activation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input label="Test input" onChange={handleChange} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Focus the input first
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(input, { key: " " });
|
||||
// Input should still be focused and ready for typing
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports Tab navigation", () => {
|
||||
render(
|
||||
<div>
|
||||
<Input label="First input" />
|
||||
<Input label="Second input" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const firstInput = screen.getByLabelText("First input");
|
||||
const secondInput = screen.getByLabelText("Second input");
|
||||
|
||||
firstInput.focus();
|
||||
expect(firstInput).toHaveFocus();
|
||||
|
||||
// Use userEvent for more realistic tab navigation
|
||||
fireEvent.keyDown(firstInput, { key: "Tab", code: "Tab" });
|
||||
// Note: In a real browser, Tab would move focus, but in tests we need to simulate it
|
||||
secondInput.focus();
|
||||
expect(secondInput).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports Shift+Tab navigation", () => {
|
||||
render(
|
||||
<div>
|
||||
<Input label="First input" />
|
||||
<Input label="Second input" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const firstInput = screen.getByLabelText("First input");
|
||||
const secondInput = screen.getByLabelText("Second input");
|
||||
|
||||
secondInput.focus();
|
||||
expect(secondInput).toHaveFocus();
|
||||
|
||||
// Use userEvent for more realistic tab navigation
|
||||
fireEvent.keyDown(secondInput, { key: "Tab", shiftKey: true, code: "Tab" });
|
||||
// Note: In a real browser, Shift+Tab would move focus, but in tests we need to simulate it
|
||||
firstInput.focus();
|
||||
expect(firstInput).toHaveFocus();
|
||||
});
|
||||
|
||||
test("handles disabled state accessibility", () => {
|
||||
render(<Input label="Test input" disabled={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
expect(input).toHaveAttribute("disabled");
|
||||
});
|
||||
|
||||
test("handles error state accessibility", () => {
|
||||
render(<Input label="Test input" error={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Error state should still be accessible
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test("supports different input types", () => {
|
||||
const { rerender } = render(<Input type="email" label="Email" />);
|
||||
let input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("type", "email");
|
||||
|
||||
rerender(<Input type="password" label="Password" />);
|
||||
// Password inputs don't have textbox role, they have textbox role only for text inputs
|
||||
input = screen.getByLabelText("Password");
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
|
||||
rerender(<Input type="number" label="Number" />);
|
||||
input = screen.getByRole("spinbutton");
|
||||
expect(input).toHaveAttribute("type", "number");
|
||||
});
|
||||
|
||||
test("supports placeholder accessibility", () => {
|
||||
render(<Input placeholder="Enter your name" />);
|
||||
const input = screen.getByPlaceholderText("Enter your name");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports value accessibility", () => {
|
||||
render(<Input value="test value" />);
|
||||
const input = screen.getByDisplayValue("test value");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("maintains focus management", () => {
|
||||
const handleFocus = vi.fn();
|
||||
const handleBlur = vi.fn();
|
||||
|
||||
render(
|
||||
<Input label="Test input" onFocus={handleFocus} onBlur={handleBlur} />,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
fireEvent.focus(input);
|
||||
expect(handleFocus).toHaveBeenCalled();
|
||||
// Focus the input to ensure it has focus
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
fireEvent.blur(input);
|
||||
expect(handleBlur).toHaveBeenCalled();
|
||||
// Manually blur the input to ensure it loses focus
|
||||
input.blur();
|
||||
expect(input).not.toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports form association", () => {
|
||||
render(
|
||||
<form>
|
||||
<Input name="test-field" label="Test input" />
|
||||
</form>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("name", "test-field");
|
||||
});
|
||||
|
||||
test("supports ARIA attributes", () => {
|
||||
render(
|
||||
<Input
|
||||
label="Test input"
|
||||
aria-describedby="help-text"
|
||||
aria-required="true"
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("aria-describedby", "help-text");
|
||||
expect(input).toHaveAttribute("aria-required", "true");
|
||||
});
|
||||
|
||||
test("supports custom ARIA labels", () => {
|
||||
render(<Input aria-label="Custom input label" />);
|
||||
const input = screen.getByLabelText("Custom input label");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles multiple inputs without conflicts", () => {
|
||||
render(
|
||||
<div>
|
||||
<Input label="First input" />
|
||||
<Input label="Second input" />
|
||||
<Input label="Third input" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const firstInput = screen.getByLabelText("First input");
|
||||
const secondInput = screen.getByLabelText("Second input");
|
||||
const thirdInput = screen.getByLabelText("Third input");
|
||||
|
||||
expect(firstInput).toBeInTheDocument();
|
||||
expect(secondInput).toBeInTheDocument();
|
||||
expect(thirdInput).toBeInTheDocument();
|
||||
|
||||
// Each should have unique IDs
|
||||
expect(firstInput.id).not.toBe(secondInput.id);
|
||||
expect(secondInput.id).not.toBe(thirdInput.id);
|
||||
expect(firstInput.id).not.toBe(thirdInput.id);
|
||||
});
|
||||
|
||||
test("supports screen reader navigation", () => {
|
||||
render(<Input label="Test input" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Test input");
|
||||
|
||||
// Label should be associated with input
|
||||
expect(label).toHaveAttribute("for", input.id);
|
||||
});
|
||||
|
||||
test("handles dynamic label changes", () => {
|
||||
const { rerender } = render(<Input label="Original label" />);
|
||||
expect(screen.getByText("Original label")).toBeInTheDocument();
|
||||
|
||||
rerender(<Input label="Updated label" />);
|
||||
expect(screen.getByText("Updated label")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Original label")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports controlled input behavior", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input value="controlled value" onChange={handleChange} />);
|
||||
|
||||
const input = screen.getByDisplayValue("controlled value");
|
||||
fireEvent.change(input, { target: { value: "new value" } });
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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)]");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user