Simplify and standardize testing structure
This commit is contained in:
@@ -1,396 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, 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 () => {
|
||||
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 () => {
|
||||
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 { 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,286 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,306 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, 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 () => {
|
||||
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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import { expect, test, describe, 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");
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import { expect, test, describe, 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();
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,158 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,234 +0,0 @@
|
||||
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 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");
|
||||
});
|
||||
});
|
||||
@@ -1,316 +0,0 @@
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { describe, test, expect, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Header from "../../../app/components/Header.js";
|
||||
import Footer from "../../../app/components/Footer.js";
|
||||
|
||||
// Extend expect to include accessibility matchers
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Accessibility - Component Level", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
// Set up proper language attribute for accessibility testing
|
||||
document.documentElement.setAttribute("lang", "en");
|
||||
});
|
||||
|
||||
test("Header component has no accessibility violations", async () => {
|
||||
const { container } = render(<Header />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Footer component has no accessibility violations", async () => {
|
||||
const { container } = render(<Footer />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Header has proper semantic structure", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check for banner landmark
|
||||
const banner = screen.getByRole("banner");
|
||||
expect(banner).toBeInTheDocument();
|
||||
|
||||
// Check for navigation landmark
|
||||
const navigation = screen.getByRole("navigation");
|
||||
expect(navigation).toBeInTheDocument();
|
||||
|
||||
// Check for proper heading structure (optional for header components)
|
||||
try {
|
||||
screen.getAllByRole("heading");
|
||||
// Headings are not required in header components, so this is optional
|
||||
} catch {
|
||||
// No headings found, which is fine for a header component
|
||||
}
|
||||
});
|
||||
|
||||
test("Header navigation items are accessible", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check that navigation items have proper roles
|
||||
const navigationItems = screen.getAllByRole("menuitem");
|
||||
expect(navigationItems.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that each navigation item has accessible text or aria-label
|
||||
navigationItems.forEach((item) => {
|
||||
const hasAccessibleText =
|
||||
item.textContent?.trim() || item.getAttribute("aria-label");
|
||||
expect(hasAccessibleText).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Header buttons have accessible names", () => {
|
||||
render(<Header />);
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
buttons.forEach((button) => {
|
||||
// Check for aria-label, aria-labelledby, or text content
|
||||
const hasAccessibleName =
|
||||
button.getAttribute("aria-label") ||
|
||||
button.getAttribute("aria-labelledby") ||
|
||||
button.textContent?.trim();
|
||||
|
||||
expect(hasAccessibleName).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Header images have alt text", () => {
|
||||
render(<Header />);
|
||||
|
||||
const images = screen.getAllByRole("img");
|
||||
images.forEach((image) => {
|
||||
const altText = image.getAttribute("alt");
|
||||
// Alt text should exist (can be empty for decorative images)
|
||||
expect(altText).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("Footer has proper semantic structure", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// Check for contentinfo landmark
|
||||
const contentinfo = screen.getByRole("contentinfo");
|
||||
expect(contentinfo).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Footer links are accessible", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
links.forEach((link) => {
|
||||
// Check for accessible text or aria-label
|
||||
const hasAccessibleText =
|
||||
link.textContent?.trim() || link.getAttribute("aria-label");
|
||||
|
||||
expect(hasAccessibleText).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Focus management works correctly", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Test that focusable elements can receive focus
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const links = screen.getAllByRole("link");
|
||||
|
||||
[...buttons, ...links].forEach((element) => {
|
||||
try {
|
||||
element.focus();
|
||||
expect(element).toHaveFocus();
|
||||
} catch {
|
||||
// Some elements might not be focusable in test environment
|
||||
// This is acceptable for accessibility testing
|
||||
// Intentionally ignore focus failures in JSDOM
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("Color contrast meets WCAG standards", async () => {
|
||||
const { container } = render(<Header />);
|
||||
const results = await axe(container, {
|
||||
rules: {
|
||||
"color-contrast": { enabled: true },
|
||||
},
|
||||
});
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Heading hierarchy is logical", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Try to get headings, but don't fail if none exist
|
||||
let headings;
|
||||
try {
|
||||
headings = screen.getAllByRole("heading");
|
||||
} catch {
|
||||
// No headings found, which is fine for a header component
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are no headings, that's fine for a header component
|
||||
if (headings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headingLevels = headings.map((heading) =>
|
||||
parseInt(heading.tagName.charAt(1)),
|
||||
);
|
||||
|
||||
// Check that heading levels are sequential (no skipping levels)
|
||||
for (let i = 1; i < headingLevels.length; i++) {
|
||||
const currentLevel = headingLevels[i];
|
||||
const previousLevel = headingLevels[i - 1];
|
||||
|
||||
// Heading levels should not skip more than one level
|
||||
expect(currentLevel - previousLevel).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("Interactive elements have proper ARIA attributes", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Get all interactive elements
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const links = screen.getAllByRole("link");
|
||||
const menuitems = screen.getAllByRole("menuitem");
|
||||
|
||||
const interactiveElements = [...buttons, ...links, ...menuitems];
|
||||
|
||||
interactiveElements.forEach((element) => {
|
||||
// Check for proper ARIA attributes
|
||||
const role = element.getAttribute("role");
|
||||
if (role) {
|
||||
// If role is specified, it should be valid
|
||||
const validRoles = [
|
||||
"button",
|
||||
"link",
|
||||
"menuitem",
|
||||
"navigation",
|
||||
"banner",
|
||||
];
|
||||
expect(validRoles).toContain(role);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("No duplicate IDs exist", async () => {
|
||||
const { container } = render(<Header />);
|
||||
const results = await axe(container, {
|
||||
rules: {
|
||||
"duplicate-id": { enabled: true },
|
||||
},
|
||||
});
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Proper language attributes", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check that the document has proper language attributes
|
||||
const html = document.documentElement;
|
||||
const lang = html.getAttribute("lang");
|
||||
expect(lang).toBeTruthy();
|
||||
expect(lang).toMatch(/^[a-z]{2}(-[A-Z]{2})?$/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user