Checkbox component with testing and storybook
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@ const config = {
|
||||
"@storybook/addon-a11y",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/nextjs-vite",
|
||||
name: "@storybook/nextjs",
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } 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 utility info 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-border-default-utility-info)]";
|
||||
|
||||
const handleToggle = (e) => {
|
||||
if (disabled) return;
|
||||
onChange?.({
|
||||
checked: !checked,
|
||||
value,
|
||||
event: e,
|
||||
});
|
||||
};
|
||||
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const checkboxId =
|
||||
id || `checkbox-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
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,78 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
|
||||
export default function FormsPlayground() {
|
||||
const [standardChecked, setStandardChecked] = useState(false);
|
||||
const [inverseChecked, setInverseChecked] = useState(true);
|
||||
|
||||
const variations = [
|
||||
{ title: "Standard / Default", mode: "standard", state: "default" },
|
||||
{ title: "Standard / Hover", mode: "standard", state: "hover" },
|
||||
{ title: "Standard / Focus", mode: "standard", state: "focus" },
|
||||
{ title: "Inverse / Default", mode: "inverse", state: "default" },
|
||||
{ title: "Inverse / Hover", mode: "inverse", state: "hover" },
|
||||
{ title: "Inverse / Focus", mode: "inverse", state: "focus" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-[24px] space-y-[24px]">
|
||||
<h1 className="font-bricolage text-[24px]">
|
||||
Forms Playground — Checkbox
|
||||
</h1>
|
||||
|
||||
<section className="space-y-[12px]">
|
||||
<h2 className="font-space text-[18px]">Interactive examples</h2>
|
||||
<div className="flex flex-col gap-[12px] max-w-[520px]">
|
||||
<Checkbox
|
||||
label="Standard (controlled)"
|
||||
checked={standardChecked}
|
||||
mode="standard"
|
||||
state="default"
|
||||
onChange={({ checked }) => setStandardChecked(checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Inverse (controlled)"
|
||||
checked={inverseChecked}
|
||||
mode="inverse"
|
||||
state="default"
|
||||
onChange={({ checked }) => setInverseChecked(checked)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-[12px]">
|
||||
<h2 className="font-space text-[18px]">Static states</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[16px]">
|
||||
{variations.map((v) => (
|
||||
<div
|
||||
key={`${v.mode}-${v.state}`}
|
||||
className="border border-[color:var(--border-color-default-tertiary)] rounded-[8px] p-[12px]"
|
||||
>
|
||||
<div className="text-[12px] mb-[8px] opacity-70">{v.title}</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
<Checkbox
|
||||
checked={false}
|
||||
mode={v.mode}
|
||||
state={v.state}
|
||||
label="Unchecked"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Checkbox
|
||||
checked
|
||||
mode={v.mode}
|
||||
state={v.state}
|
||||
label="Checked"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Generated
+4804
-4514
File diff suppressed because it is too large
Load Diff
+7
-12
@@ -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,13 @@
|
||||
"@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",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
@@ -84,14 +80,13 @@
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.2.0",
|
||||
"eslint-plugin-storybook": "^9.1.2",
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
};
|
||||
|
||||
export const Checked = {
|
||||
args: {
|
||||
checked: true,
|
||||
mode: "standard",
|
||||
state: "default",
|
||||
disabled: false,
|
||||
label: "Checked checkbox",
|
||||
},
|
||||
play: CheckedInteraction.play,
|
||||
};
|
||||
|
||||
export const Standard = {
|
||||
render: () => (
|
||||
<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={false} mode="standard" />
|
||||
<Checkbox label="Checked" checked={true} mode="standard" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
play: StandardInteraction.play,
|
||||
};
|
||||
|
||||
export const Inverse = {
|
||||
render: () => (
|
||||
<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={false} mode="inverse" />
|
||||
<Checkbox label="Checked" checked={true} mode="inverse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
play: InverseInteraction.play,
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
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,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,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,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,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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user