Switch component with storybook and testing

This commit is contained in:
adilallo
2025-10-14 17:27:09 -06:00
parent 460237fc66
commit 9de194bfc0
8 changed files with 908 additions and 262 deletions
+98
View File
@@ -0,0 +1,98 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Switch from "../../app/components/Switch";
expect.extend(toHaveNoViolations);
describe("Switch Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<Switch checked={false} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("role", "switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
});
it("has proper ARIA attributes when checked", () => {
render(<Switch checked={true} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("has proper ARIA attributes when focused", () => {
render(<Switch state="focus" label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
expect(switchButton).toHaveClass("rounded-full");
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
});
it("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("handles focus state accessibility", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("handles checked state accessibility", () => {
const { rerender } = render(<Switch checked={false} label="Test Switch" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} label="Test Switch" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("has no accessibility violations", async () => {
const { container } = render(<Switch label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when checked", async () => {
const { container } = render(<Switch checked={true} label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when focused", async () => {
const { container } = render(<Switch state="focus" label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations with text", async () => {
const { container } = render(<Switch label="Enable notifications" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations without text", async () => {
const { container } = render(<Switch />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
@@ -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();
});
});
+184
View File
@@ -0,0 +1,184 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Switch from "../../app/components/Switch";
describe("Switch Component", () => {
it("renders with default props", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toBeInTheDocument();
expect(switchButton).toHaveAttribute("aria-checked", "false");
});
it("renders with custom props", () => {
const handleChange = vi.fn();
render(
<Switch
checked={true}
onChange={handleChange}
label="Test Switch"
state="focus"
/>
);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
expect(screen.getByText("Test Switch")).toBeInTheDocument();
});
it("handles checked prop correctly", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles state prop correctly", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
fireEvent.click(switchButton);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Switch onBlur={handleBlur} />);
const switchButton = screen.getByRole("switch");
fireEvent.blur(switchButton);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(switchButton, { key: "Tab" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
});
it("applies correct track styles based on checked state", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
let track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
});
it("applies correct focus styles", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("applies correct base classes", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass(
"relative",
"inline-flex",
"items-center",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_3px_#3281F8]"
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<Switch ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<Switch className="custom-class" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("custom-class");
});
it("renders label when provided", () => {
render(<Switch label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
});
it("does not render label when not provided", () => {
render(<Switch />);
expect(screen.queryByText("Switch label")).not.toBeInTheDocument();
// Should have aria-label for accessibility
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-label", "Toggle switch");
});
it("applies correct label styles", () => {
render(<Switch label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass(
"ml-[var(--measures-spacing-008)]",
"font-inter",
"font-normal",
"text-[14px]",
"leading-[20px]",
"text-[var(--color-content-default-primary)]"
);
});
});