Simplify and standardize testing structure

This commit is contained in:
adilallo
2026-01-28 14:04:04 -07:00
parent e7a31789e3
commit 7ea724a8d9
95 changed files with 1534 additions and 15485 deletions
@@ -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();
});
});
});
-286
View File
@@ -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();
});
});
-306
View File
@@ -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)]",
);
});
});
});
-98
View File
@@ -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();
});
});
-121
View File
@@ -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");
});
});
-112
View File
@@ -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})?$/);
});
});
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import AskOrganizer from "../../app/components/AskOrganizer";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type AskOrganizerProps = React.ComponentProps<typeof AskOrganizer>;
const baseProps: AskOrganizerProps = {
title: "Need help?",
};
const config: ComponentTestSuiteConfig<AskOrganizerProps> = {
component: AskOrganizer,
name: "AskOrganizer",
props: baseProps,
optionalProps: {
subtitle: "Subtitle",
description: "Description",
buttonText: "Button",
buttonHref: "/link",
className: "custom",
variant: "centered",
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<AskOrganizerProps>(config);
describe("AskOrganizer (behavioral tests)", () => {
it("renders title", () => {
render(<AskOrganizer title="Test Title" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render(<AskOrganizer title="Test" subtitle="Subtitle" />);
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
});
it("renders button with default text", () => {
render(<AskOrganizer title="Test" />);
expect(
screen.getByRole("link", {
name: /ask an organizer/i,
}),
).toBeInTheDocument();
});
it("renders button with custom text", () => {
render(
<AskOrganizer title="Test" buttonText="Contact" buttonHref="/contact" />,
);
expect(
screen.getByRole("link", {
name: /contact/i,
}),
).toBeInTheDocument();
});
});
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import Button from "../../app/components/Button";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type ButtonProps = React.ComponentProps<typeof Button>;
const baseProps: ButtonProps = {
children: "Click me",
};
const config: ComponentTestSuiteConfig<ButtonProps> = {
component: Button,
name: "Button",
props: baseProps,
requiredProps: ["children"],
optionalProps: {
href: "/test",
ariaLabel: "Accessible button",
},
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
};
componentTestSuite<ButtonProps>(config);
describe("Button (behavioral tests)", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole("button", { name: "Click me" });
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("renders as a link when href is provided", () => {
render(
<Button href="/learn" variant="default">
Learn more
</Button>,
);
const link = screen.getByRole("link", { name: "Learn more" });
expect(link).toHaveAttribute("href", "/learn");
});
});
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import Checkbox from "../../app/components/Checkbox";
import { componentTestSuite } from "../utils/componentTestSuite";
type CheckboxProps = React.ComponentProps<typeof Checkbox>;
componentTestSuite<CheckboxProps>({
component: Checkbox,
name: "Checkbox",
props: {
label: "Test checkbox",
} as CheckboxProps,
requiredProps: ["label"],
optionalProps: {
value: "test",
},
primaryRole: "checkbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+45
View File
@@ -0,0 +1,45 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ContentBanner from "../../app/components/ContentBanner";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("../../lib/assetUtils", () => ({
getAssetPath: vi.fn((asset: string) => `/assets/${asset}`),
}));
const mockPost: BlogPost = {
slug: "test-article",
frontmatter: {
title: "Test Article",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
};
describe("ContentBanner", () => {
it("renders without crashing", () => {
render(<ContentBanner post={mockPost} />);
});
it("renders article title", () => {
render(<ContentBanner post={mockPost} />);
expect(
screen.getByRole("heading", { name: "Test Article" }),
).toBeInTheDocument();
});
it("renders article description", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test description")).toBeInTheDocument();
});
});
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import { componentTestSuite } from "../utils/componentTestSuite";
type ContextMenuProps = React.ComponentProps<typeof ContextMenu>;
componentTestSuite<ContextMenuProps>({
component: ContextMenu,
name: "ContextMenu",
props: {
children: (
<ContextMenuItem>
Item
</ContextMenuItem>
),
} as ContextMenuProps,
requiredProps: [],
primaryRole: "menu",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import { componentTestSuite } from "../utils/componentTestSuite";
type ContextMenuItemProps = React.ComponentProps<typeof ContextMenuItem>;
componentTestSuite<ContextMenuItemProps>({
component: ContextMenuItem,
name: "ContextMenuItem",
props: {
children: "Item",
} as ContextMenuItemProps,
requiredProps: [],
primaryRole: "menuitem",
testCases: {
renders: true,
accessibility: false,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+80
View File
@@ -0,0 +1,80 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import FeatureGrid from "../../app/components/FeatureGrid";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type FeatureGridProps = React.ComponentProps<typeof FeatureGrid>;
const baseProps: FeatureGridProps = {
title: "Feature Tools",
subtitle: "Everything you need",
};
const config: ComponentTestSuiteConfig<FeatureGridProps> = {
component: FeatureGrid,
name: "FeatureGrid",
props: baseProps,
optionalProps: {
className: "custom-class",
title: undefined,
subtitle: undefined,
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<FeatureGridProps>(config);
describe("FeatureGrid (behavioral tests)", () => {
it("renders title and subtitle", () => {
render(<FeatureGrid title="Test Title" subtitle="Test Subtitle" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Test Subtitle" }),
).toBeInTheDocument();
});
it("renders all four feature cards", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
expect(
screen.getByRole("link", { name: "Decision-making support tools" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Values alignment exercises" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Membership guidance resources" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Conflict resolution tools" }),
).toBeInTheDocument();
});
it("has proper accessibility attributes", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveAttribute("aria-labelledby", "feature-grid-headline");
expect(screen.getByRole("grid")).toHaveAttribute(
"aria-label",
"Feature tools and services",
);
});
it("handles missing props gracefully", () => {
render(<FeatureGrid />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
});
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Footer from "../../app/components/Footer";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type FooterProps = React.ComponentProps<typeof Footer>;
const baseProps: FooterProps = {};
const config: ComponentTestSuiteConfig<FooterProps> = {
component: Footer,
name: "Footer",
props: baseProps,
primaryRole: "contentinfo",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false, // Footer is not primarily keyboard navigable
disabledState: false,
errorState: false,
},
};
componentTestSuite<FooterProps>(config);
describe("Footer (behavioral tests)", () => {
it("renders organization schema markup", () => {
render(<Footer />);
const script = document.querySelector('script[type="application/ld+json"]');
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script?.textContent || "{}");
expect(schemaData["@type"]).toBe("Organization");
expect(schemaData.name).toBe("Media Economies Design Lab");
});
it("renders organization name and contact", () => {
render(<Footer />);
expect(
screen.getAllByText("Media Economies Design Lab").length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "medlab@colorado.edu" }).length,
).toBeGreaterThan(0);
});
it("renders social media links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Follow us on Bluesky" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Follow us on GitLab" }).length,
).toBeGreaterThan(0);
});
it("renders navigation links", () => {
render(<Footer />);
expect(screen.getAllByRole("link", { name: "Use cases" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "Learn" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "About" }).length).toBeGreaterThan(0);
});
it("renders legal links", () => {
render(<Footer />);
expect(screen.getAllByRole("link", { name: "Privacy Policy" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "Terms of Service" }).length).toBeGreaterThan(0);
});
});
+22
View File
@@ -0,0 +1,22 @@
import React from "react";
import Header from "../../app/components/Header";
import { componentTestSuite } from "../utils/componentTestSuite";
type HeaderProps = React.ComponentProps<typeof Header>;
componentTestSuite<HeaderProps>({
component: Header,
name: "Header",
// Header has no props; it reads from routing and config.
props: {} as HeaderProps,
requiredProps: [],
primaryRole: "banner",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type HeroBannerProps = React.ComponentProps<typeof HeroBanner>;
const baseProps: HeroBannerProps = {
title: "Welcome",
};
const config: ComponentTestSuiteConfig<HeroBannerProps> = {
component: HeroBanner,
name: "HeroBanner",
props: baseProps,
optionalProps: {
subtitle: "Subtitle",
description: "Description",
ctaText: "CTA",
ctaHref: "/link",
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<HeroBannerProps>(config);
describe("HeroBanner (behavioral tests)", () => {
it("renders title", () => {
render(<HeroBanner title="Test Title" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render(<HeroBanner title="Test" subtitle="Subtitle" />);
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
});
it("renders hero image", () => {
render(<HeroBanner title="Test" />);
expect(
screen.getByRole("img", { name: "Hero illustration" }),
).toBeInTheDocument();
});
it("renders CTA button when provided", () => {
render(
<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />,
);
expect(
screen.getAllByRole("button", { name: "Get Started" }).length,
).toBeGreaterThan(0);
});
});
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import Input from "../../app/components/Input";
import { componentTestSuite } from "../utils/componentTestSuite";
type InputProps = React.ComponentProps<typeof Input>;
componentTestSuite<InputProps>({
component: Input,
name: "Input",
props: {
label: "Test input",
} as InputProps,
requiredProps: ["label"],
optionalProps: {
placeholder: "Enter value",
},
primaryRole: "textbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+68
View File
@@ -0,0 +1,68 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Logo from "../../app/components/Logo";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type LogoProps = React.ComponentProps<typeof Logo>;
const baseProps: LogoProps = {};
const config: ComponentTestSuiteConfig<LogoProps> = {
component: Logo,
name: "Logo",
props: baseProps,
primaryRole: "link",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: false,
errorState: false,
},
};
componentTestSuite<LogoProps>(config);
describe("Logo (behavioral tests)", () => {
it("renders as a link to home", () => {
render(<Logo />);
const logo = screen.getByRole("link", { name: /communityrule logo/i });
expect(logo).toHaveAttribute("href", "/");
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
});
it("renders logo icon", () => {
render(<Logo />);
expect(
screen.getByAltText("CommunityRule Logo Icon"),
).toBeInTheDocument();
});
it("renders text by default", () => {
render(<Logo />);
expect(screen.getByText("CommunityRule")).toBeInTheDocument();
});
it("hides text when showText is false", () => {
render(<Logo showText={false} />);
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
expect(
screen.getByAltText("CommunityRule Logo Icon"),
).toBeInTheDocument();
});
it("renders with different size variants", () => {
const { rerender } = render(<Logo size="header" />);
expect(screen.getByRole("link")).toBeInTheDocument();
rerender(<Logo size="footer" />);
expect(screen.getByRole("link")).toBeInTheDocument();
rerender(<Logo size="homeHeaderMd" />);
expect(screen.getByRole("link")).toBeInTheDocument();
});
});
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import RadioButton from "../../app/components/RadioButton";
import { componentTestSuite } from "../utils/componentTestSuite";
type RadioButtonProps = React.ComponentProps<typeof RadioButton>;
componentTestSuite<RadioButtonProps>({
component: RadioButton,
name: "RadioButton",
props: {
label: "Option A",
checked: false,
} as RadioButtonProps,
requiredProps: [],
optionalProps: {
mode: "inverse",
},
primaryRole: "radio",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import RadioGroup from "../../app/components/RadioGroup";
import { componentTestSuite } from "../utils/componentTestSuite";
type RadioGroupProps = React.ComponentProps<typeof RadioGroup>;
componentTestSuite<RadioGroupProps>({
component: RadioGroup,
name: "RadioGroup",
props: {
name: "example",
value: "a",
options: [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
],
} as RadioGroupProps,
requiredProps: [],
primaryRole: "radiogroup",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+81
View File
@@ -0,0 +1,81 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import RelatedArticles from "../../app/components/RelatedArticles";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post }: { post: BlogPost }) => (
<div data-testid={`thumbnail-${post.slug}`}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter.title}</h3>
</a>
</div>
),
}));
vi.mock("../../app/hooks", () => ({
useIsMobile: () => false,
}));
const mockPosts: BlogPost[] = [
{
slug: "article-1",
frontmatter: {
title: "Article 1",
description: "Description 1",
author: "Author",
date: "2025-04-10",
},
},
{
slug: "article-2",
frontmatter: {
title: "Article 2",
description: "Description 2",
author: "Author",
date: "2025-04-11",
},
},
];
describe("RelatedArticles", () => {
it("renders without crashing", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
/>,
);
});
it("renders related articles", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
/>,
);
expect(screen.getByTestId("thumbnail-article-1")).toBeInTheDocument();
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
});
it("filters out current post", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="article-1"
/>,
);
expect(screen.queryByTestId("thumbnail-article-1")).not.toBeInTheDocument();
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
});
});
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import SectionHeader from "../../app/components/SectionHeader";
import { componentTestSuite } from "../utils/componentTestSuite";
type SectionHeaderProps = React.ComponentProps<typeof SectionHeader>;
componentTestSuite<SectionHeaderProps>({
component: SectionHeader,
name: "SectionHeader",
props: {
title: "Title",
subtitle: "Subtitle",
} as SectionHeaderProps,
requiredProps: ["title", "subtitle"],
primaryRole: "heading",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
import Select from "../../app/components/Select";
import { componentTestSuite } from "../utils/componentTestSuite";
type SelectProps = React.ComponentProps<typeof Select>;
componentTestSuite<SelectProps>({
component: Select,
name: "Select",
props: {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
],
} as SelectProps,
requiredProps: ["options"],
optionalProps: {
size: "medium",
},
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import Switch from "../../app/components/Switch";
import { componentTestSuite } from "../utils/componentTestSuite";
type SwitchProps = React.ComponentProps<typeof Switch>;
componentTestSuite<SwitchProps>({
component: Switch,
name: "Switch",
props: {
label: "Test Switch",
} as SwitchProps,
requiredProps: [],
optionalProps: {
state: "focus",
},
primaryRole: "switch",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+31
View File
@@ -0,0 +1,31 @@
import React from "react";
import TextArea from "../../app/components/TextArea";
import { componentTestSuite } from "../utils/componentTestSuite";
type TextAreaProps = React.ComponentProps<typeof TextArea>;
componentTestSuite<TextAreaProps>({
component: TextArea,
name: "TextArea",
props: {
label: "Description",
value: "",
} as TextAreaProps,
requiredProps: ["label"],
optionalProps: {
placeholder: "Enter description",
},
primaryRole: "textbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+27
View File
@@ -0,0 +1,27 @@
import React from "react";
import Toggle from "../../app/components/Toggle";
import { componentTestSuite } from "../utils/componentTestSuite";
type ToggleProps = React.ComponentProps<typeof Toggle>;
componentTestSuite<ToggleProps>({
component: Toggle,
name: "Toggle",
props: {
label: "Notifications",
checked: false,
} as ToggleProps,
requiredProps: [],
primaryRole: "switch",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import ToggleGroup from "../../app/components/ToggleGroup";
import { componentTestSuite } from "../utils/componentTestSuite";
type ToggleGroupProps = React.ComponentProps<typeof ToggleGroup>;
componentTestSuite<ToggleGroupProps>({
component: ToggleGroup,
name: "ToggleGroup",
props: {
children: "Option",
} as ToggleGroupProps,
requiredProps: [],
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: false,
errorState: false,
},
});
-136
View File
@@ -1,136 +0,0 @@
import { test, expect } from "@playwright/test";
const breakpoints = [
{ name: "xs", width: 320, height: 568 },
{ name: "sm", width: 640, height: 720 },
{ name: "md", width: 768, height: 1024 },
{ name: "lg", width: 1024, height: 768 },
{ name: "xl", width: 1280, height: 800 },
{ name: "2xl", width: 1536, height: 864 },
{ name: "3xl", width: 1920, height: 1080 },
{ name: "4xl", width: 2560, height: 1440 },
{ name: "full", width: 3840, height: 2160 },
];
test.describe("Footer responsive behavior", () => {
for (const bp of breakpoints) {
test(`footer content visibility at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test that footer is visible
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
// Test navigation links
await expect(
page.getByRole("contentinfo").getByRole("link", { name: /use cases/i }),
).toBeVisible();
await expect(
page.getByRole("contentinfo").getByRole("link", { name: /learn/i }),
).toBeVisible();
await expect(
page.getByRole("contentinfo").getByRole("link", { name: /about/i }),
).toBeVisible();
// Test legal links
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /privacy policy/i }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /terms of service/i }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /cookies settings/i }),
).toBeVisible();
// Test social links
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /follow us on bluesky/i }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /follow us on gitlab/i }),
).toBeVisible();
});
test(`footer layout consistency at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test that footer has proper structure
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
// Test that footer contains expected sections
// Note: Logo visibility can vary by breakpoint due to responsive design
// We'll skip this test to avoid flakiness
// await expect(footer.getByText("CommunityRule")).toBeVisible();
});
}
test.describe("Footer interaction states", () => {
test("hover states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test hover on navigation items
const useCasesLink = page
.getByRole("contentinfo")
.getByRole("link", { name: /use cases/i });
await useCasesLink.hover();
await page.waitForTimeout(200);
// Test hover on social links
const blueskyLink = page.getByRole("contentinfo").getByRole("link", {
name: /follow us on bluesky/i,
});
await blueskyLink.hover();
await page.waitForTimeout(200);
});
test("focus states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test focus on navigation items
const useCasesLink = page
.getByRole("contentinfo")
.getByRole("link", { name: /use cases/i });
await useCasesLink.focus();
await page.waitForTimeout(200);
// Test focus on social links
const blueskyLink = page.getByRole("contentinfo").getByRole("link", {
name: /follow us on bluesky/i,
});
await blueskyLink.focus();
await page.waitForTimeout(200);
});
});
});
-195
View File
@@ -1,195 +0,0 @@
import { test, expect } from "@playwright/test";
const breakpoints = [
{ name: "xs", width: 320, height: 568 },
{ name: "sm", width: 640, height: 720 },
{ name: "md", width: 768, height: 1024 },
{ name: "lg", width: 1024, height: 768 },
{ name: "xl", width: 1280, height: 800 },
{ name: "2xl", width: 1536, height: 864 },
{ name: "3xl", width: 1920, height: 1080 },
{ name: "4xl", width: 2560, height: 1440 },
{ name: "full", width: 3840, height: 2160 },
];
test.describe("Header responsive behavior", () => {
for (const bp of breakpoints) {
test(`navigation items visibility at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// All breakpoints should have navigation items
await expect(
page.getByRole("menuitem", { name: /use cases/i }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: /learn/i }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: /about/i }),
).toBeVisible();
});
test(`login and create rule button visibility at ${bp.name}`, async ({
page,
}) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// All breakpoints should have login button
await expect(
page.getByRole("menuitem", { name: /log in to your account/i }),
).toBeVisible();
// All breakpoints should have create rule button
await expect(
page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
}),
).toBeVisible();
});
test(`header layout consistency at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Test that header is visible and has proper structure
const header = page.locator("header").first();
await expect(header).toBeVisible();
// Test that header contains navigation
await expect(header.getByRole("navigation")).toBeVisible();
// Test that header contains logo/brand
// Note: Logo visibility can vary by breakpoint due to responsive design
// We'll skip this test to avoid flakiness
// await expect(header.getByText("CommunityRule")).toBeVisible();
});
}
test.describe("Header interaction states", () => {
test("hover states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Test hover on navigation items
const useCasesLink = page.getByRole("menuitem", { name: /use cases/i });
await useCasesLink.hover();
await page.waitForTimeout(200);
// Test hover on create rule button
const createRuleButton = page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
});
await createRuleButton.hover();
await page.waitForTimeout(200);
});
test("focus states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Test focus on navigation items
const useCasesLink = page.getByRole("menuitem", { name: /use cases/i });
await useCasesLink.focus();
await page.waitForTimeout(200);
// Test focus on create rule button
const createRuleButton = page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
});
await createRuleButton.focus();
await page.waitForTimeout(200);
});
});
test.describe("Header sticky behavior", () => {
test("regular header is sticky on non-home pages", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/learn");
const header = page.locator("header").first();
// Check that header has sticky positioning
const headerStyles = await header.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
position: computed.position,
top: computed.top,
zIndex: computed.zIndex,
};
});
expect(headerStyles.position).toBe("sticky");
expect(headerStyles.top).toBe("0px");
expect(headerStyles.zIndex).toBe("50");
});
test("home header is not sticky on home page", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
const header = page.locator("header").first();
// Check that header does not have sticky positioning
const headerStyles = await header.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
position: computed.position,
top: computed.top,
zIndex: computed.zIndex,
};
});
expect(headerStyles.position).not.toBe("sticky");
});
});
test.describe("Active navigation state", () => {
test("learn page shows active state for learn navigation", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/learn");
const learnLink = page.getByRole("menuitem", { name: /learn/i });
// Check that learn link has active styling
const linkStyles = await learnLink.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
outline: computed.outline,
outlineColor: computed.outlineColor,
color: computed.color,
};
});
// Should have outline and brand color
expect(linkStyles.outline).not.toBe("none");
expect(linkStyles.outlineColor).toContain("254, 252, 201"); // RGB value of #fefcc9
});
test("home page does not show active state for learn navigation", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
const learnLink = page.getByRole("menuitem", { name: /learn/i });
// Check that learn link does not have active styling
const linkStyles = await learnLink.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
outline: computed.outline,
outlineColor: computed.outlineColor,
};
});
// Should not have active outline (may have default browser outline)
expect(linkStyles.outline).toMatch(
/^(none|0px|rgb\(0, 0, 0\) none 0px|rgb\(0, 0, 0\) 0px)$/,
);
});
});
});
@@ -1,171 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles";
// Mock ContentThumbnailTemplate with a simple implementation
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post, variant }) => (
<div data-testid={`thumbnail-${post.slug}`} data-variant={variant}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter?.title || "Untitled"}</h3>
<p>{post.frontmatter?.description || "No description"}</p>
</a>
</div>
),
}));
// Mock blog post data
const mockRelatedPosts = [
{
slug: "resolving-active-conflicts",
frontmatter: {
title: "Resolving Active Conflicts",
description:
"Practical steps for resolving conflicts while maintaining trust",
author: "Test Author",
date: "2025-04-15",
},
},
{
slug: "operational-security-mutual-aid",
frontmatter: {
title: "Operational Security for Mutual Aid",
description:
"Tactics to protect members, secure communication, and prevent infiltration",
author: "Test Author",
date: "2025-04-14",
},
},
{
slug: "making-decisions-without-hierarchy",
frontmatter: {
title: "Making Decisions Without Hierarchy",
description:
"A brief guide to collaborative nonhierarchical decision making",
author: "Test Author",
date: "2025-04-13",
},
},
];
describe("Blog Core Integration", () => {
beforeEach(() => {
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024, // Desktop width
});
});
it("should render RelatedArticles component with correct structure", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Verify the section exists
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
// Verify thumbnails are rendered
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
// Current post should not be displayed
expect(
screen.queryByTestId("thumbnail-resolving-active-conflicts"),
).not.toBeInTheDocument();
});
it("should filter out current post from related articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Current post should not be displayed
expect(
screen.queryByTestId("thumbnail-resolving-active-conflicts"),
).not.toBeInTheDocument();
// Other posts should be displayed
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
});
it("should display all posts when no current post is specified", () => {
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// All posts should be displayed
expect(
screen.getByTestId("thumbnail-resolving-active-conflicts"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
});
it("should handle empty related posts array", () => {
const { container } = render(
<RelatedArticles relatedPosts={[]} currentPostSlug="test-post" />,
);
expect(container.firstChild).toBeNull();
});
it("should create correct links for each thumbnail", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Verify links are created correctly
const operationalLink = screen
.getByTestId("thumbnail-operational-security-mutual-aid")
.querySelector("a");
const hierarchyLink = screen
.getByTestId("thumbnail-making-decisions-without-hierarchy")
.querySelector("a");
expect(operationalLink).toHaveAttribute(
"href",
"/blog/operational-security-mutual-aid",
);
expect(hierarchyLink).toHaveAttribute(
"href",
"/blog/making-decisions-without-hierarchy",
);
});
it("should display section heading", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
});
});
@@ -1,249 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import Checkbox from "../../app/components/Checkbox";
// Test component that uses Checkbox in a form
function TestForm() {
const [formData, setFormData] = useState({
agree: false,
newsletter: false,
notifications: true,
});
const handleCheckboxChange =
(field) =>
({ checked }) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Form submission logic would go here
};
return (
<form onSubmit={handleSubmit} data-testid="test-form">
<Checkbox
label="I agree to the terms"
checked={formData.agree}
onChange={handleCheckboxChange("agree")}
name="agree"
data-testid="agree-checkbox"
/>
<Checkbox
label="Subscribe to newsletter"
checked={formData.newsletter}
onChange={handleCheckboxChange("newsletter")}
name="newsletter"
data-testid="newsletter-checkbox"
/>
<Checkbox
label="Enable notifications"
checked={formData.notifications}
onChange={handleCheckboxChange("notifications")}
name="notifications"
data-testid="notifications-checkbox"
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
}
describe("Checkbox Integration Tests", () => {
test("handles multiple checkboxes in a form", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const notificationsCheckbox = screen.getByTestId("notifications-checkbox");
// Initial state
expect(agreeCheckbox).toHaveAttribute("aria-checked", "false");
expect(newsletterCheckbox).toHaveAttribute("aria-checked", "false");
expect(notificationsCheckbox).toHaveAttribute("aria-checked", "true");
// Toggle checkboxes
await user.click(agreeCheckbox);
await user.click(newsletterCheckbox);
await user.click(notificationsCheckbox);
// Check final state
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
expect(newsletterCheckbox).toHaveAttribute("aria-checked", "true");
expect(notificationsCheckbox).toHaveAttribute("aria-checked", "false");
});
test("handles keyboard navigation between checkboxes", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const notificationsCheckbox = screen.getByTestId("notifications-checkbox");
// Focus first checkbox
await user.tab();
expect(agreeCheckbox).toHaveFocus();
// Navigate to next checkbox
await user.tab();
expect(newsletterCheckbox).toHaveFocus();
// Navigate to next checkbox
await user.tab();
expect(notificationsCheckbox).toHaveFocus();
});
test("handles keyboard activation", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
// Focus and activate with Space
await user.tab();
expect(agreeCheckbox).toHaveFocus();
expect(agreeCheckbox).toHaveAttribute("aria-checked", "false");
await user.keyboard(" ");
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
// Activate with Enter
await user.keyboard("Enter");
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
});
test("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [checked, setChecked] = useState(false);
return (
<div>
<Checkbox
label="Switch to inverse mode"
checked={mode === "inverse"}
onChange={({ checked }) =>
setMode(checked ? "inverse" : "standard")
}
data-testid="mode-switch"
/>
<Checkbox
label="Test checkbox"
checked={checked}
onChange={({ checked }) => setChecked(checked)}
mode={mode}
data-testid="test-checkbox"
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const modeSwitch = screen.getByTestId("mode-switch");
const testCheckbox = screen.getByTestId("test-checkbox");
// Initially standard mode
expect(testCheckbox).toBeInTheDocument();
// Switch to inverse mode
await user.click(modeSwitch);
expect(testCheckbox).toBeInTheDocument();
// Should still be functional
await user.click(testCheckbox);
expect(testCheckbox).toHaveAttribute("aria-checked", "true");
});
test("handles form submission with checkbox values", async () => {
const handleSubmit = vi.fn();
function FormWithSubmission() {
const [formData, setFormData] = useState({
agree: false,
newsletter: false,
});
const handleCheckboxChange =
(field) =>
({ checked }) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const onSubmit = (e) => {
e.preventDefault();
handleSubmit(formData);
};
return (
<form onSubmit={onSubmit} data-testid="form">
<Checkbox
label="I agree"
checked={formData.agree}
onChange={handleCheckboxChange("agree")}
name="agree"
value="yes"
data-testid="agree-checkbox"
/>
<Checkbox
label="Newsletter"
checked={formData.newsletter}
onChange={handleCheckboxChange("newsletter")}
name="newsletter"
value="yes"
data-testid="newsletter-checkbox"
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
}
const user = userEvent.setup();
render(<FormWithSubmission />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const submitButton = screen.getByTestId("submit-button");
// Check some checkboxes
await user.click(agreeCheckbox);
await user.click(newsletterCheckbox);
// Submit form
await user.click(submitButton);
// Verify form data was captured
expect(handleSubmit).toHaveBeenCalledWith({
agree: true,
newsletter: true,
});
});
test("handles accessibility in form context", async () => {
render(<TestForm />);
const form = screen.getByTestId("test-form");
const checkboxes = screen.getAllByRole("checkbox");
// All checkboxes should be accessible
expect(checkboxes).toHaveLength(3);
checkboxes.forEach((checkbox) => {
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked");
expect(checkbox).toHaveAttribute("tabIndex");
});
// Form should be accessible
expect(form).toBeInTheDocument();
});
});
@@ -1,156 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import ContentLockup from "../../app/components/ContentLockup";
afterEach(() => {
cleanup();
});
describe("ContentLockup Integration", () => {
test("renders hero variant with all content", () => {
render(
<ContentLockup
variant="hero"
title="Welcome"
subtitle="Get Started"
description="This is a description"
ctaText="Get Started"
/>,
);
expect(
screen.getByRole("heading", { name: "Welcome" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Get Started" }),
).toBeInTheDocument();
expect(screen.getByText("This is a description")).toBeInTheDocument();
expect(screen.getAllByRole("button", { name: "Get Started" })).toHaveLength(
3,
);
});
test("renders feature variant with link", () => {
render(
<ContentLockup
variant="feature"
title="Feature Title"
subtitle="Feature Subtitle"
description="Feature description"
linkText="Learn More"
linkHref="/learn"
/>,
);
expect(
screen.getByRole("heading", { name: "Feature Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Learn More" }),
).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Learn More" })).toHaveAttribute(
"href",
"/learn",
);
});
test("renders ask variant with simplified structure", () => {
render(
<ContentLockup
variant="ask"
title="Ask Question"
subtitle="Ask subtitle"
/>,
);
expect(
screen.getByRole("heading", { name: "Ask Question" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Ask subtitle" }),
).toBeInTheDocument();
// Ask variant should not have description or CTA
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
test("handles left alignment", () => {
render(
<ContentLockup
variant="ask"
title="Left Aligned"
subtitle="Subtitle"
alignment="left"
/>,
);
const container = screen
.getByRole("heading", { name: "Left Aligned" })
.closest("div");
expect(container).toHaveClass("justify-start");
});
test("renders responsive buttons correctly", () => {
render(
<ContentLockup variant="hero" title="Responsive" ctaText="Click Me" />,
);
// Should render all three button variants for different breakpoints
const buttons = screen.getAllByRole("button", { name: "Click Me" });
expect(buttons).toHaveLength(3);
});
test("applies custom button className", () => {
render(
<ContentLockup
variant="hero"
title="Custom Button"
ctaText="Custom"
buttonClassName="custom-button-class"
/>,
);
const buttons = screen.getAllByRole("button", { name: "Custom" });
// The large button (md breakpoint) should have the custom class
expect(buttons[1]).toHaveClass("custom-button-class");
});
test("handles missing optional props gracefully", () => {
render(<ContentLockup variant="hero" title="Minimal" />);
expect(
screen.getByRole("heading", { name: "Minimal" }),
).toBeInTheDocument();
// Should not crash without subtitle, description, or CTA
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
test("renders decorative shape for hero variant", () => {
render(<ContentLockup variant="hero" title="Hero with Shape" />);
const shape = screen.getByRole("presentation");
expect(shape).toBeInTheDocument();
expect(shape).toHaveAttribute("src", "/assets/Shapes_1.svg");
expect(shape).toHaveAttribute("alt", "");
});
test("does not render shape for non-hero variants", () => {
render(<ContentLockup variant="feature" title="Feature without Shape" />);
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
test("link has proper accessibility attributes", () => {
render(
<ContentLockup
variant="feature"
title="Accessible"
linkText="Accessible Link"
linkHref="/accessible"
/>,
);
const link = screen.getByRole("link", { name: "Accessible Link" });
expect(link).toHaveAttribute("href", "/accessible");
expect(link).toHaveClass("focus:outline-none", "focus:ring-2");
});
});
@@ -1,384 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi } from "vitest";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
describe("ContextMenu Components Integration", () => {
const TestMenu = ({ onItemClick, selectedValue }) => (
<ContextMenu>
<ContextMenuSection title="Actions">
<ContextMenuItem
onClick={() => onItemClick("action1")}
selected={selectedValue === "action1"}
>
Action 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => onItemClick("action2")}
selected={selectedValue === "action2"}
>
Action 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Settings">
<ContextMenuItem
onClick={() => onItemClick("setting1")}
hasSubmenu={true}
>
Setting 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => onItemClick("setting2")}
disabled={true}
>
Setting 2
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
describe("Menu Interaction", () => {
it("handles item selection correctly", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const action1 = screen.getByText("Action 1");
await user.click(action1);
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("shows selected state correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="action1" />);
const action1 = screen.getByRole("menuitem", { name: "Action 1" });
expect(action1).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
);
});
it("handles disabled items correctly", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const setting2 = screen.getByText("Setting 2");
await user.click(setting2);
expect(onItemClick).not.toHaveBeenCalled();
});
it("shows submenu indicators correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const arrow = screen
.getByRole("menuitem", { name: "Setting 1" })
.querySelector("svg");
expect(arrow).toBeInTheDocument();
});
});
describe("Keyboard Navigation", () => {
it("navigates through menu items with arrow keys", async () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(4);
// Check that enabled items are focusable and disabled items are not
const enabledItems = items.filter(
(item) =>
!item.hasAttribute("aria-disabled") ||
item.getAttribute("aria-disabled") !== "true",
);
const disabledItems = items.filter(
(item) => item.getAttribute("aria-disabled") === "true",
);
enabledItems.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
disabledItems.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "-1");
});
});
it("selects items with Enter key", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard("{Enter}");
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("selects items with Space key", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard(" ");
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("skips disabled items during navigation", async () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(4);
// Check that disabled items have tabIndex="-1"
const disabledItem = screen.getByRole("menuitem", { name: "Setting 2" });
expect(disabledItem).toHaveAttribute("tabIndex", "-1");
expect(disabledItem).toHaveAttribute("aria-disabled", "true");
});
});
describe("Dynamic Menu Updates", () => {
const DynamicMenu = ({ items, selectedValue, onItemClick }) => (
<ContextMenu>
{items.map((item) => (
<ContextMenuItem
key={item.id}
onClick={() => onItemClick(item.id)}
selected={selectedValue === item.id}
disabled={item.disabled}
>
{item.label}
</ContextMenuItem>
))}
</ContextMenu>
);
it("handles dynamic item updates", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
const { rerender } = render(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
]}
selectedValue=""
onItemClick={onItemClick}
/>,
);
const item1 = screen.getByText("Item 1");
await user.click(item1);
expect(onItemClick).toHaveBeenCalledWith("1");
// Update items
rerender(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
{ id: "3", label: "Item 3" },
]}
selectedValue="1"
onItemClick={onItemClick}
/>,
);
expect(screen.getByText("Item 3")).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Item 1" })).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
);
});
it("handles item removal", () => {
const { rerender } = render(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
{ id: "3", label: "Item 3" },
]}
selectedValue="2"
onItemClick={vi.fn()}
/>,
);
expect(screen.getByText("Item 2")).toBeInTheDocument();
rerender(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "3", label: "Item 3" },
]}
selectedValue=""
onItemClick={vi.fn()}
/>,
);
expect(screen.queryByText("Item 2")).not.toBeInTheDocument();
});
});
describe("Menu State Management", () => {
const StatefulMenu = () => {
const [selectedValue, setSelectedValue] = useState("");
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Close Menu" : "Open Menu"}
</button>
{isOpen && (
<ContextMenu>
<ContextMenuItem
onClick={() => {
setSelectedValue("option1");
setIsOpen(false);
}}
selected={selectedValue === "option1"}
>
Option 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => {
setSelectedValue("option2");
setIsOpen(false);
}}
selected={selectedValue === "option2"}
>
Option 2
</ContextMenuItem>
</ContextMenu>
)}
</div>
);
};
it("manages menu open/close state", async () => {
const user = userEvent.setup();
render(<StatefulMenu />);
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
await user.click(toggleButton);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Close Menu" }),
).toBeInTheDocument();
});
it("closes menu after selection", async () => {
const user = userEvent.setup();
render(<StatefulMenu />);
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
await user.click(toggleButton);
const option1 = screen.getByText("Option 1");
await user.click(option1);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Open Menu" }),
).toBeInTheDocument();
});
});
describe("Performance", () => {
it("handles large menu lists efficiently", async () => {
const largeItems = Array.from({ length: 100 }, (_, i) => ({
id: `item${i}`,
label: `Item ${i}`,
}));
const LargeMenu = () => (
<ContextMenu>
{largeItems.map((item) => (
<ContextMenuItem key={item.id} onClick={vi.fn()}>
{item.label}
</ContextMenuItem>
))}
</ContextMenu>
);
render(<LargeMenu />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(100);
// Test that all items are focusable
items.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
});
it("handles rapid state changes", async () => {
const { rerender } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={false}>
Item 1
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={false}>
Item 2
</ContextMenuItem>
</ContextMenu>,
);
// Rapidly change selection state
for (let i = 0; i < 10; i++) {
rerender(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={i % 2 === 0}>
Item 1
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={i % 2 === 1}>
Item 2
</ContextMenuItem>
</ContextMenu>,
);
}
// Should still be functional
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(2);
});
});
describe("Error Handling", () => {
it("handles missing onClick gracefully", () => {
render(
<ContextMenu>
<ContextMenuItem>Item without onClick</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByText("Item without onClick");
expect(item).toBeInTheDocument();
});
it("handles invalid props gracefully", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={null}>
Item with invalid selected
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByText("Item with invalid selected");
expect(item).toBeInTheDocument();
});
});
});
@@ -1,426 +0,0 @@
import React, { useState } from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input";
// Test component that uses Input with state management
const TestInputForm = ({ initialValue = "", onValueChange }) => {
const [value, setValue] = useState(initialValue);
const [focused, setFocused] = useState(false);
const handleChange = (e) => {
setValue(e.target.value);
onValueChange?.(e.target.value);
};
const handleFocus = () => setFocused(true);
const handleBlur = () => setFocused(false);
return (
<div>
<Input
label="Test Input"
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
state={focused ? "focus" : "default"}
/>
<div data-testid="value-display">{value}</div>
<div data-testid="focus-status">{focused ? "focused" : "blurred"}</div>
</div>
);
};
// Test component with multiple inputs
const MultiInputForm = () => {
const [values, setValues] = useState({
firstName: "",
lastName: "",
email: "",
});
const handleChange = (field) => (e) => {
setValues((prev) => ({ ...prev, [field]: e.target.value }));
};
return (
<form>
<Input
label="First Name"
name="firstName"
value={values.firstName}
onChange={handleChange("firstName")}
/>
<Input
label="Last Name"
name="lastName"
value={values.lastName}
onChange={handleChange("lastName")}
/>
<Input
label="Email"
name="email"
type="email"
value={values.email}
onChange={handleChange("email")}
/>
</form>
);
};
// Test component with validation
const ValidatedInputForm = () => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
setValue(e.target.value);
setError(e.target.value.length < 3);
};
return (
<div>
<Input
label="Required Field"
value={value}
onChange={handleChange}
error={error}
/>
{error && (
<div data-testid="error-message">Minimum 3 characters required</div>
)}
</div>
);
};
describe("Input Component Integration", () => {
test("handles controlled input with state management", async () => {
const onValueChange = vi.fn();
render(<TestInputForm onValueChange={onValueChange} />);
const input = screen.getByLabelText("Test Input");
const valueDisplay = screen.getByTestId("value-display");
const focusStatus = screen.getByTestId("focus-status");
// Initial state
expect(valueDisplay).toHaveTextContent("");
expect(focusStatus).toHaveTextContent("blurred");
// Type in input
fireEvent.change(input, { target: { value: "test value" } });
expect(valueDisplay).toHaveTextContent("test value");
expect(onValueChange).toHaveBeenCalledWith("test value");
// Focus input
fireEvent.focus(input);
expect(focusStatus).toHaveTextContent("focused");
// Blur input
fireEvent.blur(input);
expect(focusStatus).toHaveTextContent("blurred");
});
test("handles multiple inputs independently", () => {
render(<MultiInputForm />);
const firstNameInput = screen.getByLabelText("First Name");
const lastNameInput = screen.getByLabelText("Last Name");
const emailInput = screen.getByLabelText("Email");
// Type in first input
fireEvent.change(firstNameInput, { target: { value: "John" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("");
expect(emailInput).toHaveValue("");
// Type in second input
fireEvent.change(lastNameInput, { target: { value: "Doe" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("Doe");
expect(emailInput).toHaveValue("");
// Type in third input
fireEvent.change(emailInput, { target: { value: "john@example.com" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("Doe");
expect(emailInput).toHaveValue("john@example.com");
});
test("handles form validation", () => {
render(<ValidatedInputForm />);
const input = screen.getByLabelText("Required Field");
const errorMessage = screen.queryByTestId("error-message");
// Initial state - no error
expect(errorMessage).not.toBeInTheDocument();
// Type short value - should show error
fireEvent.change(input, { target: { value: "ab" } });
expect(screen.getByTestId("error-message")).toBeInTheDocument();
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
// Type longer value - should hide error
fireEvent.change(input, { target: { value: "abc" } });
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
});
test("handles different input types", () => {
render(
<div>
<Input label="Text Input" type="text" />
<Input label="Email Input" type="email" />
<Input label="Password Input" type="password" />
<Input label="Number Input" type="number" />
</div>,
);
const textInput = screen.getByLabelText("Text Input");
const emailInput = screen.getByLabelText("Email Input");
const passwordInput = screen.getByLabelText("Password Input");
const numberInput = screen.getByLabelText("Number Input");
expect(textInput).toHaveAttribute("type", "text");
expect(emailInput).toHaveAttribute("type", "email");
expect(passwordInput).toHaveAttribute("type", "password");
expect(numberInput).toHaveAttribute("type", "number");
});
test("handles different sizes and label variants", () => {
render(
<div>
<Input label="Small Default" size="small" labelVariant="default" />
<Input
label="Small Horizontal"
size="small"
labelVariant="horizontal"
/>
<Input label="Medium Default" size="medium" labelVariant="default" />
<Input
label="Medium Horizontal"
size="medium"
labelVariant="horizontal"
/>
<Input label="Large Default" size="large" labelVariant="default" />
<Input
label="Large Horizontal"
size="large"
labelVariant="horizontal"
/>
</div>,
);
// All inputs should be present
expect(screen.getByLabelText("Small Default")).toBeInTheDocument();
expect(screen.getByLabelText("Small Horizontal")).toBeInTheDocument();
expect(screen.getByLabelText("Medium Default")).toBeInTheDocument();
expect(screen.getByLabelText("Medium Horizontal")).toBeInTheDocument();
expect(screen.getByLabelText("Large Default")).toBeInTheDocument();
expect(screen.getByLabelText("Large Horizontal")).toBeInTheDocument();
});
test("handles disabled state integration", () => {
const handleChange = vi.fn();
render(
<Input
label="Disabled Input"
disabled={true}
onChange={handleChange}
onFocus={vi.fn()}
onBlur={vi.fn()}
/>,
);
const input = screen.getByLabelText("Disabled Input");
// Should be disabled
expect(input).toBeDisabled();
// Should not call handlers
fireEvent.change(input, { target: { value: "test" } });
fireEvent.focus(input);
fireEvent.blur(input);
expect(handleChange).not.toHaveBeenCalled();
});
test("handles error state integration", () => {
render(<Input label="Error Input" error={true} />);
const input = screen.getByLabelText("Error Input");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
expect(input).not.toBeDisabled();
});
test("handles state transitions", async () => {
const TestStateTransitions = () => {
const [state, setState] = useState("default");
return (
<div>
<Input
label="State Test"
state={state}
onFocus={() => setState("focus")}
onBlur={() => setState("default")}
/>
<button onClick={() => setState("hover")}>Set Hover</button>
<button onClick={() => setState("active")}>Set Active</button>
</div>
);
};
render(<TestStateTransitions />);
const input = screen.getByLabelText("State Test");
const hoverButton = screen.getByText("Set Hover");
const activeButton = screen.getByText("Set Active");
// Initial state
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
// Set hover state
fireEvent.click(hoverButton);
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
// Set active state
fireEvent.click(activeButton);
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
// Focus state
fireEvent.focus(input);
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
test("handles keyboard navigation between inputs", () => {
render(
<div>
<Input label="First Input" />
<Input label="Second Input" />
<Input label="Third Input" />
</div>,
);
const firstInput = screen.getByLabelText("First Input");
const secondInput = screen.getByLabelText("Second Input");
const thirdInput = screen.getByLabelText("Third Input");
// Focus first input
firstInput.focus();
expect(firstInput).toHaveFocus();
// Tab to second input - simulate actual tab behavior
fireEvent.keyDown(firstInput, { key: "Tab" });
// Manually focus the second input since tab navigation doesn't work in jsdom
secondInput.focus();
expect(secondInput).toHaveFocus();
// Tab to third input
fireEvent.keyDown(secondInput, { key: "Tab" });
// Manually focus the third input
thirdInput.focus();
expect(thirdInput).toHaveFocus();
// Shift+Tab back to second input
fireEvent.keyDown(thirdInput, { key: "Tab", shiftKey: true });
// Manually focus the second input
secondInput.focus();
expect(secondInput).toHaveFocus();
});
test("handles form submission", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Input label="Test Input" name="testField" />
<button type="submit">Submit</button>
</form>,
);
const input = screen.getByLabelText("Test Input");
const submitButton = screen.getByText("Submit");
// Type in input
fireEvent.change(input, { target: { value: "test value" } });
// Submit form
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
test("handles ref forwarding", () => {
const TestRefComponent = () => {
const inputRef = React.useRef();
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} label="Ref Test" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
render(<TestRefComponent />);
const input = screen.getByLabelText("Ref Test");
const focusButton = screen.getByText("Focus Input");
// Click button to focus input via ref
fireEvent.click(focusButton);
expect(input).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const TestDynamicProps = () => {
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState(false);
return (
<div>
<Input label="Dynamic Input" disabled={disabled} error={error} />
<button onClick={() => setDisabled(!disabled)}>
Toggle Disabled
</button>
<button onClick={() => setError(!error)}>Toggle Error</button>
</div>
);
};
render(<TestDynamicProps />);
const input = screen.getByLabelText("Dynamic Input");
const toggleDisabledButton = screen.getByText("Toggle Disabled");
const toggleErrorButton = screen.getByText("Toggle Error");
// Initial state
expect(input).not.toBeDisabled();
expect(input).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
// Toggle disabled
fireEvent.click(toggleDisabledButton);
expect(input).toBeDisabled();
// Toggle error - but first disable the disabled state so error can be tested
fireEvent.click(toggleDisabledButton); // Turn off disabled
fireEvent.click(toggleErrorButton); // Turn on error
// The error state applies the border color through the stateStyles.input class
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
});
@@ -1,365 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../app/components/RadioButton";
describe("RadioButton Integration", () => {
it("works in form context", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
function TestForm() {
const [value, setValue] = useState("option1");
return (
<form onSubmit={handleSubmit}>
<RadioButton
label="Option 1"
name="test-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
<RadioButton
label="Option 2"
name="test-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
/>
<button type="submit">Submit</button>
</form>
);
}
render(<TestForm />);
const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button");
// Initially option1 should be selected
expect(screen.getByDisplayValue("option1")).toBeChecked();
expect(screen.getByDisplayValue("option2")).not.toBeChecked();
// Click option2
await user.click(option2);
expect(screen.getByDisplayValue("option2")).toBeChecked();
expect(screen.getByDisplayValue("option1")).not.toBeChecked();
// Submit form
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
function KeyboardForm() {
const [value, setValue] = useState("option1");
return (
<div>
<RadioButton
label="Option 1"
name="keyboard-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
<RadioButton
label="Option 2"
name="keyboard-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
/>
</div>
);
}
render(<KeyboardForm />);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await user.tab();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(screen.getByDisplayValue("option2")).toBeChecked();
});
it("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [value, setValue] = useState("option1");
return (
<div>
<button
onClick={() =>
setMode(mode === "standard" ? "inverse" : "standard")
}
>
Toggle Mode
</button>
<RadioButton
label="Test Radio"
name="mode-radio"
value="option1"
checked={value === "option1"}
mode={mode}
onChange={({ checked }) => checked && setValue("option1")}
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const toggleButton = screen.getByRole("button");
const radioButton = screen.getByRole("radio");
// Initially standard mode
expect(radioButton).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
// Switch to inverse mode
await user.click(toggleButton);
expect(radioButton).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
it("maintains state across re-renders", () => {
function StateForm() {
const [value, setValue] = useState("option1");
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render ({count})
</button>
<RadioButton
label="Test Radio"
name="state-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
</div>
);
}
const user = userEvent.setup();
render(<StateForm />);
const radioButton = screen.getByRole("radio");
const reRenderButton = screen.getByRole("button");
// Should be checked initially
expect(radioButton).toHaveAttribute("aria-checked", "true");
// Re-render should maintain state
user.click(reRenderButton);
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("works with multiple radio groups", async () => {
function MultiGroupForm() {
const [group1Value, setGroup1Value] = useState("option1");
const [group2Value, setGroup2Value] = useState("option1");
return (
<div>
<div>
<h3>Group 1</h3>
<RadioButton
label="Option A"
name="group1"
value="option1"
checked={group1Value === "option1"}
onChange={({ checked }) => checked && setGroup1Value("option1")}
/>
<RadioButton
label="Option B"
name="group1"
value="option2"
checked={group1Value === "option2"}
onChange={({ checked }) => checked && setGroup1Value("option2")}
/>
</div>
<div>
<h3>Group 2</h3>
<RadioButton
label="Option X"
name="group2"
value="option1"
checked={group2Value === "option1"}
onChange={({ checked }) => checked && setGroup2Value("option1")}
/>
<RadioButton
label="Option Y"
name="group2"
value="option2"
checked={group2Value === "option2"}
onChange={({ checked }) => checked && setGroup2Value("option2")}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<MultiGroupForm />);
// Both groups should work independently
const group1OptionB = screen.getByText("Option B").closest("label");
const group2OptionY = screen.getByText("Option Y").closest("label");
await user.click(group1OptionB);
await user.click(group2OptionY);
const group1Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group1");
const group2Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group2");
expect(group1Inputs[0]).toBeChecked();
expect(group2Inputs[0]).toBeChecked();
});
it("handles controlled and uncontrolled scenarios", async () => {
function ControlledForm() {
const [controlledValue, setControlledValue] = useState("option1");
const [uncontrolledValue, setUncontrolledValue] = useState("option1");
return (
<div>
<div>
<h3>Controlled</h3>
<RadioButton
label="Controlled Option 1"
name="controlled"
value="option1"
checked={controlledValue === "option1"}
onChange={({ checked }) =>
checked && setControlledValue("option1")
}
/>
<RadioButton
label="Controlled Option 2"
name="controlled"
value="option2"
checked={controlledValue === "option2"}
onChange={({ checked }) =>
checked && setControlledValue("option2")
}
/>
</div>
<div>
<h3>Uncontrolled</h3>
<RadioButton
label="Uncontrolled Option 1"
name="uncontrolled"
value="option1"
checked={uncontrolledValue === "option1"}
onChange={({ checked }) =>
checked && setUncontrolledValue("option1")
}
/>
<RadioButton
label="Uncontrolled Option 2"
name="uncontrolled"
value="option2"
checked={uncontrolledValue === "option2"}
onChange={({ checked }) =>
checked && setUncontrolledValue("option2")
}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<ControlledForm />);
// Both should work the same way
const controlledOption2 = screen
.getByText("Controlled Option 2")
.closest("label");
const uncontrolledOption2 = screen
.getByText("Uncontrolled Option 2")
.closest("label");
await user.click(controlledOption2);
await user.click(uncontrolledOption2);
const controlledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "controlled");
const uncontrolledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "uncontrolled");
expect(controlledInputs[0]).toBeChecked();
expect(uncontrolledInputs[0]).toBeChecked();
});
it("handles accessibility in complex forms", () => {
function AccessibleForm() {
const [value, setValue] = useState("option1");
return (
<form>
<fieldset>
<legend>Choose an option</legend>
<RadioButton
label="Option 1"
name="accessible-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
ariaLabel="First option"
/>
<RadioButton
label="Option 2"
name="accessible-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
ariaLabel="Second option"
/>
</fieldset>
</form>
);
}
render(<AccessibleForm />);
const radioButtons = screen.getAllByRole("radio");
// Should have proper accessibility attributes
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have aria-labels
expect(radioButtons[0]).toHaveAttribute("aria-label", "First option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second option");
});
});
@@ -1,430 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../app/components/RadioGroup";
describe("RadioGroup Integration", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("works in form context", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
function TestForm() {
const [value, setValue] = useState("option1");
return (
<form onSubmit={handleSubmit}>
<RadioGroup
name="test-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
<button type="submit">Submit</button>
</form>
);
}
render(<TestForm />);
const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button");
// Initially option1 should be selected
expect(screen.getByDisplayValue("option1")).toBeChecked();
expect(screen.getByDisplayValue("option2")).not.toBeChecked();
// Click option2
await user.click(option2);
expect(screen.getByDisplayValue("option2")).toBeChecked();
expect(screen.getByDisplayValue("option1")).not.toBeChecked();
// Submit form
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
function KeyboardForm() {
const [value, setValue] = useState("option1");
return (
<RadioGroup
name="keyboard-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
);
}
render(<KeyboardForm />);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await user.tab();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(screen.getByDisplayValue("option2")).toBeChecked();
});
it("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [value, setValue] = useState("option1");
return (
<div>
<button
onClick={() =>
setMode(mode === "standard" ? "inverse" : "standard")
}
>
Toggle Mode
</button>
<RadioGroup
name="mode-radio-group"
value={value}
mode={mode}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const toggleButton = screen.getByRole("button");
const radioButtons = screen.getAllByRole("radio");
// Initially standard mode
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
// Switch to inverse mode
await user.click(toggleButton);
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
});
it("maintains state across re-renders", () => {
function StateForm() {
const [value, setValue] = useState("option1");
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render ({count})
</button>
<RadioGroup
name="state-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<StateForm />);
const radioButtons = screen.getAllByRole("radio");
const reRenderButton = screen.getByRole("button");
// Should be checked initially
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
// Re-render should maintain state
user.click(reRenderButton);
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
});
it("works with multiple radio groups", async () => {
function MultiGroupForm() {
const [group1Value, setGroup1Value] = useState("option1");
const [group2Value, setGroup2Value] = useState("option1");
return (
<div>
<div>
<h3>Group 1</h3>
<RadioGroup
name="group1"
value={group1Value}
options={defaultOptions}
onChange={({ value }) => setGroup1Value(value)}
/>
</div>
<div>
<h3>Group 2</h3>
<RadioGroup
name="group2"
value={group2Value}
options={defaultOptions}
onChange={({ value }) => setGroup2Value(value)}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<MultiGroupForm />);
// Both groups should work independently
// Find the Option 2 in group1 by filtering getAllByDisplayValue by name
const group1Option2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "group1");
const group1Option2 = group1Option2Input.closest("label");
// Find the Option 3 in group2 by filtering getAllByDisplayValue by name
const group2Option3Input = screen
.getAllByDisplayValue("option3")
.find((input) => input.getAttribute("name") === "group2");
const group2Option3 = group2Option3Input.closest("label");
await user.click(group1Option2);
await user.click(group2Option3);
const group1Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group1");
const group2Inputs = screen
.getAllByDisplayValue("option3")
.filter((input) => input.getAttribute("name") === "group2");
expect(group1Inputs[0]).toBeChecked();
expect(group2Inputs[0]).toBeChecked();
});
it("handles controlled and uncontrolled scenarios", async () => {
function ControlledForm() {
const [controlledValue, setControlledValue] = useState("option1");
const [uncontrolledValue, setUncontrolledValue] = useState("option1");
return (
<div>
<div>
<h3>Controlled</h3>
<RadioGroup
name="controlled"
value={controlledValue}
options={defaultOptions}
onChange={({ value }) => setControlledValue(value)}
/>
</div>
<div>
<h3>Uncontrolled</h3>
<RadioGroup
name="uncontrolled"
value={uncontrolledValue}
options={defaultOptions}
onChange={({ value }) => setUncontrolledValue(value)}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<ControlledForm />);
// Both should work the same way
// Find the Option 2 in controlled group by filtering getAllByDisplayValue by name
const controlledOption2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "controlled");
const controlledOption2 = controlledOption2Input.closest("label");
// Find the Option 2 in uncontrolled group by filtering getAllByDisplayValue by name
const uncontrolledOption2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "uncontrolled");
const uncontrolledOption2 = uncontrolledOption2Input.closest("label");
await user.click(controlledOption2);
await user.click(uncontrolledOption2);
const controlledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "controlled");
const uncontrolledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "uncontrolled");
expect(controlledInputs[0]).toBeChecked();
expect(uncontrolledInputs[0]).toBeChecked();
});
it("handles accessibility in complex forms", () => {
function AccessibleForm() {
const [value, setValue] = useState("option1");
const accessibleOptions = [
{ value: "option1", label: "Option 1", ariaLabel: "First option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second option" },
{ value: "option3", label: "Option 3", ariaLabel: "Third option" },
];
return (
<form>
<fieldset>
<legend>Choose an option</legend>
<RadioGroup
name="accessible-radio-group"
value={value}
options={accessibleOptions}
onChange={({ value }) => setValue(value)}
aria-label="Accessible radio group"
/>
</fieldset>
</form>
);
}
render(<AccessibleForm />);
const radioGroup = screen.getByRole("radiogroup");
const radioButtons = screen.getAllByRole("radio");
// Should have proper accessibility attributes
expect(radioGroup).toHaveAttribute("aria-label", "Accessible radio group");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have aria-labels
expect(radioButtons[0]).toHaveAttribute("aria-label", "First option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second option");
expect(radioButtons[2]).toHaveAttribute("aria-label", "Third option");
});
it("handles dynamic options", async () => {
function DynamicForm() {
const [value, setValue] = useState("option1");
const [options, setOptions] = useState(defaultOptions);
return (
<div>
<button
onClick={() =>
setOptions([...options, { value: "option4", label: "Option 4" }])
}
>
Add Option
</button>
<RadioGroup
name="dynamic-radio-group"
value={value}
options={options}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<DynamicForm />);
const addButton = screen.getByRole("button");
// Initially 3 options
expect(screen.getAllByRole("radio")).toHaveLength(3);
// Add option
await user.click(addButton);
expect(screen.getAllByRole("radio")).toHaveLength(4);
expect(screen.getByText("Option 4")).toBeInTheDocument();
});
it("handles empty options gracefully", () => {
function EmptyForm() {
const [value, setValue] = useState("");
return (
<RadioGroup
name="empty-radio-group"
value={value}
options={[]}
onChange={({ value }) => setValue(value)}
/>
);
}
render(<EmptyForm />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
expect(screen.queryAllByRole("radio")).toHaveLength(0);
});
it("maintains single selection behavior", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
function SingleSelectionForm() {
const [value, setValue] = useState("option1");
return (
<RadioGroup
name="single-selection-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => {
setValue(value);
handleChange(value);
}}
/>
);
}
render(<SingleSelectionForm />);
const radioButtons = screen.getAllByRole("radio");
// Initially option1 should be selected
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click option2
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
// Only option2 should be selected
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
expect(handleChange).toHaveBeenCalledWith("option2");
});
});
@@ -1,214 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles";
// Mock ContentThumbnailTemplate
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post, variant }) => (
<div data-testid={`thumbnail-${post.slug}`} data-variant={variant}>
<a
href={`/blog/${post.slug}`}
data-testid={`thumbnail-link-${post.slug}`}
>
<h3>{post.frontmatter?.title || "Untitled"}</h3>
<p>{post.frontmatter?.description || "No description"}</p>
</a>
</div>
),
}));
// Mock blog post data
const mockRelatedPosts = [
{
slug: "resolving-active-conflicts",
frontmatter: {
title: "Resolving Active Conflicts",
description:
"Practical steps for resolving conflicts while maintaining trust",
author: "Test Author",
date: "2025-04-15",
},
},
{
slug: "operational-security-mutual-aid",
frontmatter: {
title: "Operational Security for Mutual Aid",
description:
"Tactics to protect members, secure communication, and prevent infiltration",
author: "Test Author",
date: "2025-04-14",
},
},
{
slug: "making-decisions-without-hierarchy",
frontmatter: {
title: "Making Decisions Without Hierarchy",
description:
"A brief guide to collaborative nonhierarchical decision making",
author: "Test Author",
date: "2025-04-13",
},
},
{
slug: "building-community-trust",
frontmatter: {
title: "Building Community Trust",
description: "Strategies for fostering trust in community organizations",
author: "Test Author",
date: "2025-04-12",
},
},
];
describe("Related Articles Integration", () => {
beforeEach(() => {
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024, // Desktop width
});
});
it("should filter out current post from related articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Current post should not be displayed
expect(
screen.queryByTestId("thumbnail-resolving-active-conflicts"),
).not.toBeInTheDocument();
// Other posts should be displayed
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-building-community-trust"),
).toBeInTheDocument();
});
it("should display all posts when no current post is specified", () => {
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// All posts should be displayed
expect(
screen.getByTestId("thumbnail-resolving-active-conflicts"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-building-community-trust"),
).toBeInTheDocument();
});
it("should create correct links for each thumbnail", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Verify links are created correctly
expect(
screen.getByTestId("thumbnail-link-operational-security-mutual-aid"),
).toHaveAttribute("href", "/blog/operational-security-mutual-aid");
expect(
screen.getByTestId("thumbnail-link-making-decisions-without-hierarchy"),
).toHaveAttribute("href", "/blog/making-decisions-without-hierarchy");
expect(
screen.getByTestId("thumbnail-link-building-community-trust"),
).toHaveAttribute("href", "/blog/building-community-trust");
});
it("should handle empty related posts array", () => {
const { container } = render(
<RelatedArticles relatedPosts={[]} currentPostSlug="test-post" />,
);
expect(container.firstChild).toBeNull();
});
it("should handle single related post", () => {
const singlePost = [mockRelatedPosts[0]];
render(
<RelatedArticles
relatedPosts={singlePost}
currentPostSlug="different-post"
/>,
);
expect(
screen.getByTestId("thumbnail-resolving-active-conflicts"),
).toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-operational-security-mutual-aid"),
).not.toBeInTheDocument();
});
it("should handle all posts being filtered out", () => {
const currentPostOnly = [mockRelatedPosts[0]];
const { container } = render(
<RelatedArticles
relatedPosts={currentPostOnly}
currentPostSlug="resolving-active-conflicts"
/>,
);
expect(container.firstChild).toBeNull();
});
it("should display section heading", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
});
it("should maintain consistent structure across different current posts", () => {
const slugs = [
"resolving-active-conflicts",
"operational-security-mutual-aid",
"making-decisions-without-hierarchy",
];
slugs.forEach((slug) => {
const { unmount } = render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug={slug}
/>,
);
// Verify consistent structure
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
// Check that we have some thumbnails (the exact ones depend on the current post)
const thumbnails = screen.getAllByTestId(/thumbnail-/);
expect(thumbnails.length).toBeGreaterThan(0);
unmount();
});
});
});
@@ -1,407 +0,0 @@
import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it } from "vitest";
import Select from "../../app/components/Select";
describe("Select Component Integration", () => {
const TestForm = ({ initialValue = "" }) => {
const [value, setValue] = useState(initialValue);
const [errors, setErrors] = useState({});
const handleChange = (newValue) => {
setValue(newValue);
if (errors.select) {
setErrors({ ...errors, select: null });
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (!value) {
setErrors({ select: "Please select an option" });
}
};
return (
<form onSubmit={handleSubmit}>
<Select
label="Test Select"
placeholder="Select an option"
value={value}
onChange={handleChange}
error={!!errors.select}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
{errors.select && <div data-testid="error">{errors.select}</div>}
<button type="submit">Submit</button>
</form>
);
};
describe("Form Integration", () => {
it("integrates with form submission", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
it("shows validation error when no option selected", async () => {
const user = userEvent.setup();
render(<TestForm />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.getByTestId("error")).toHaveTextContent(
"Please select an option",
);
});
it("clears error when option is selected", async () => {
const user = userEvent.setup();
render(<TestForm />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.getByTestId("error")).toBeInTheDocument();
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
});
describe("Multiple Select Components", () => {
const MultiSelectForm = () => {
const [values, setValues] = useState({ select1: "", select2: "" });
const handleChange = (field) => (newValue) => {
setValues({ ...values, [field]: newValue });
};
return (
<div>
<Select
label="First Select"
placeholder="Select first option"
value={values.select1}
onChange={handleChange("select1")}
options={[
{ value: "a1", label: "A1" },
{ value: "a2", label: "A2" },
]}
/>
<Select
label="Second Select"
placeholder="Select second option"
value={values.select2}
onChange={handleChange("select2")}
options={[
{ value: "b1", label: "B1" },
{ value: "b2", label: "B2" },
]}
/>
</div>
);
};
it("handles multiple select components independently", async () => {
const user = userEvent.setup();
render(<MultiSelectForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.click(firstSelect);
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
await user.click(screen.getByText("A1"));
await user.click(secondSelect);
await waitFor(() => {
expect(screen.getByText("B1")).toBeInTheDocument();
});
await user.click(screen.getByText("B1"));
expect(firstSelect).toHaveTextContent("A1");
expect(secondSelect).toHaveTextContent("B1");
});
it("closes one dropdown when another is opened", async () => {
const user = userEvent.setup();
render(<MultiSelectForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.click(firstSelect);
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
await user.click(secondSelect);
await waitFor(() => {
expect(screen.queryByText("A1")).not.toBeInTheDocument();
expect(screen.getByText("B1")).toBeInTheDocument();
});
});
});
describe("Keyboard Navigation Between Components", () => {
const KeyboardForm = () => {
const [values, setValues] = useState({ select1: "", select2: "" });
return (
<div>
<input placeholder="First input" />
<Select
label="First Select"
placeholder="Select first option"
value={values.select1}
onChange={(value) => setValues({ ...values, select1: value })}
options={[{ value: "a1", label: "A1" }]}
/>
<input placeholder="Second input" />
<Select
label="Second Select"
placeholder="Select second option"
value={values.select2}
onChange={(value) => setValues({ ...values, select2: value })}
options={[{ value: "b1", label: "B1" }]}
/>
</div>
);
};
it("handles keyboard navigation between inputs and selects", async () => {
const user = userEvent.setup();
render(<KeyboardForm />);
const firstInput = screen.getByPlaceholderText("First input");
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondInput = screen.getByPlaceholderText("Second input");
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.tab();
expect(firstInput).toHaveFocus();
await user.tab();
expect(firstSelect).toHaveFocus();
await user.tab();
expect(secondInput).toHaveFocus();
await user.tab();
expect(secondSelect).toHaveFocus();
});
it("opens select with Enter key during tab navigation", async () => {
const user = userEvent.setup();
render(<KeyboardForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
await user.tab();
await user.tab();
expect(firstSelect).toHaveFocus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
});
});
describe("Dynamic Prop Changes", () => {
const DynamicSelect = ({ disabled, error, size }) => {
const [value, setValue] = useState("");
return (
<Select
label="Dynamic Select"
placeholder="Select an option"
value={value}
onChange={setValue}
disabled={disabled}
error={error}
size={size}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
]}
/>
);
};
it("handles dynamic disabled state changes", async () => {
const { rerender } = render(<DynamicSelect disabled={false} />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).not.toBeDisabled();
rerender(<DynamicSelect disabled={true} />);
expect(selectButton).toBeDisabled();
rerender(<DynamicSelect disabled={false} />);
expect(selectButton).not.toBeDisabled();
});
it("handles dynamic error state changes", async () => {
const { rerender } = render(<DynamicSelect error={false} />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
rerender(<DynamicSelect error={true} />);
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
rerender(<DynamicSelect error={false} />);
expect(selectButton).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
it("handles dynamic size changes", async () => {
const { rerender } = render(<DynamicSelect size="small" />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).toHaveClass("h-[32px]");
rerender(<DynamicSelect size="medium" />);
expect(selectButton).toHaveClass("h-[36px]");
rerender(<DynamicSelect size="large" />);
expect(selectButton).toHaveClass("h-[40px]");
});
});
describe("Focus State Behavior", () => {
it("enters focus state when tabbed to (not active state)", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.tab();
expect(selectButton).toHaveFocus();
// Should have focus state styling, not active state
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
it("does not enter focus state when clicked", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
expect(selectButton).toHaveFocus();
// Click should not trigger focus-visible styles (class is always present but only active on keyboard focus)
// The focus-visible class is always in the component but only applies on keyboard focus
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
});
describe("Performance", () => {
it("handles rapid state changes without issues", async () => {
const user = userEvent.setup();
const { rerender } = render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
// Rapidly change props
for (let i = 0; i < 10; i++) {
rerender(<TestForm />);
await user.click(selectButton);
await user.keyboard("{Escape}");
}
// Should still be functional
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("handles large option lists efficiently", async () => {
const user = userEvent.setup();
const largeOptions = Array.from({ length: 100 }, (_, i) => ({
value: `option${i}`,
label: `Option ${i}`,
}));
render(
<Select
label="Large Select"
placeholder="Select an option"
options={largeOptions}
/>,
);
const selectButton = screen.getByRole("button", { name: /Large Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 0")).toBeInTheDocument();
expect(screen.getByText("Option 99")).toBeInTheDocument();
});
});
});
});
@@ -1,265 +0,0 @@
import React from "react";
import { render, screen, 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();
});
});
@@ -1,280 +0,0 @@
import React from "react";
import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
// Test form component for integration testing
const TestForm = () => {
const [formData, setFormData] = React.useState({
textarea1: "",
textarea2: "",
});
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<TextArea
label="First TextArea"
name="textarea1"
value={formData.textarea1}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea1: e.target.value }))
}
placeholder="Enter first text..."
/>
<TextArea
label="Second TextArea"
name="textarea2"
value={formData.textarea2}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea2: e.target.value }))
}
placeholder="Enter second text..."
/>
<button type="submit">Submit</button>
</form>
);
};
// Dynamic TextArea component for prop changes testing
const DynamicTextArea = ({ size, labelVariant, state, disabled, error }) => {
const [value, setValue] = React.useState("");
return (
<TextArea
label="Dynamic TextArea"
value={value}
onChange={(e) => setValue(e.target.value)}
size={size}
labelVariant={labelVariant}
state={state}
disabled={disabled}
error={error}
placeholder="Enter text..."
/>
);
};
describe("TextArea Integration Tests", () => {
test("handles form submission with multiple textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
const submitButton = screen.getByRole("button", { name: /Submit/ });
await user.type(firstTextarea, "First content");
await user.type(secondTextarea, "Second content");
expect(firstTextarea).toHaveValue("First content");
expect(secondTextarea).toHaveValue("Second content");
await user.click(submitButton);
// Form submission should not cause errors
});
test("handles keyboard navigation between textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
await user.click(firstTextarea);
expect(firstTextarea).toHaveFocus();
await user.tab();
expect(secondTextarea).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<DynamicTextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<DynamicTextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<DynamicTextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("handles state changes", () => {
const { rerender } = render(<DynamicTextArea state="default" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]",
);
});
test("handles disabled state changes", () => {
const { rerender } = render(<DynamicTextArea disabled={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).not.toBeDisabled();
rerender(<DynamicTextArea disabled={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("handles error state changes", () => {
const { rerender } = render(<DynamicTextArea error={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea error={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("handles label variant changes", () => {
const { rerender } = render(<DynamicTextArea labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
rerender(<DynamicTextArea labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("handles text input and changes", async () => {
const user = userEvent.setup();
render(<DynamicTextArea />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "Hello World");
expect(textarea).toHaveValue("Hello World");
});
test("handles focus and blur events", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
const handleBlur = vi.fn();
render(
<TextArea
label="Test TextArea"
onFocus={handleFocus}
onBlur={handleBlur}
/>,
);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("handles multiple textareas with different configurations", () => {
render(
<div>
<TextArea
size="small"
label="Small TextArea"
placeholder="Small placeholder"
/>
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
placeholder="Medium placeholder"
/>
<TextArea
size="large"
label="Large TextArea"
placeholder="Large placeholder"
/>
</div>,
);
expect(
screen.getByPlaceholderText("Small placeholder"),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Medium placeholder"),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Large placeholder"),
).toBeInTheDocument();
});
test("handles form validation with error states", () => {
render(
<div>
<TextArea label="Valid TextArea" placeholder="Valid input" />
<TextArea label="Invalid TextArea" placeholder="Invalid input" error />
<TextArea
label="Disabled TextArea"
placeholder="Disabled input"
disabled
/>
</div>,
);
const validTextarea = screen.getByPlaceholderText("Valid input");
const invalidTextarea = screen.getByPlaceholderText("Invalid input");
const disabledTextarea = screen.getByPlaceholderText("Disabled input");
expect(validTextarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
expect(invalidTextarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
expect(disabledTextarea).toBeDisabled();
});
test("handles performance with multiple re-renders", () => {
const { rerender } = render(<DynamicTextArea />);
// Simulate multiple re-renders
for (let i = 0; i < 10; i++) {
rerender(<DynamicTextArea size={i % 2 === 0 ? "small" : "large"} />);
}
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("handles accessibility with screen readers", async () => {
const user = userEvent.setup();
render(<TextArea label="Accessible TextArea" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Accessible TextArea");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
await user.click(textarea);
expect(textarea).toHaveFocus();
});
});
@@ -1,185 +0,0 @@
import React from "react";
import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Toggle from "../../app/components/Toggle";
describe("Toggle Integration", () => {
test("handles form submission", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Toggle label="Test Toggle" name="toggle" />
<button type="submit">Submit</button>
</form>,
);
const toggle = screen.getByRole("switch", { name: "Test Toggle" });
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(toggle);
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
test("handles keyboard navigation between toggles", async () => {
const user = userEvent.setup();
render(
<div>
<Toggle label="First Toggle" />
<Toggle label="Second Toggle" />
<Toggle label="Third Toggle" />
</div>,
);
const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
const thirdToggle = screen.getByRole("switch", { name: "Third Toggle" });
await user.tab();
expect(firstToggle).toHaveFocus();
await user.tab();
expect(secondToggle).toHaveFocus();
await user.tab();
expect(thirdToggle).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
rerender(<Toggle label="Test Toggle" disabled={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
});
test("handles multiple toggles in form", () => {
const handleChange1 = vi.fn();
const handleChange2 = vi.fn();
render(
<div>
<Toggle label="First Toggle" onChange={handleChange1} />
<Toggle label="Second Toggle" onChange={handleChange2} />
</div>,
);
const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
fireEvent.click(firstToggle);
expect(handleChange1).toHaveBeenCalledTimes(1);
expect(handleChange2).not.toHaveBeenCalled();
fireEvent.click(secondToggle);
expect(handleChange2).toHaveBeenCalledTimes(1);
expect(handleChange1).toHaveBeenCalledTimes(1);
});
test("handles state changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" state="default" />);
let toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Toggle label="Test Toggle" state="focus" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
test("handles content changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" />);
let toggle = screen.getByRole("switch");
expect(toggle).not.toHaveTextContent("I");
expect(toggle).not.toHaveTextContent("Toggle");
rerender(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
rerender(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("Toggle");
rerender(
<Toggle
label="Test Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>,
);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
expect(toggle).toHaveTextContent("Toggle");
});
test("handles performance with many toggles", () => {
const toggles = Array.from({ length: 100 }, (_, i) => (
<Toggle key={i} label={`Toggle ${i}`} />
));
const startTime = performance.now();
render(<div>{toggles}</div>);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(1000); // Should render in less than 1 second
expect(screen.getAllByRole("switch")).toHaveLength(100);
});
test("handles rapid state changes", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
// Rapid clicks
for (let i = 0; i < 10; i++) {
fireEvent.click(toggle);
}
expect(handleChange).toHaveBeenCalledTimes(10);
});
test("handles mixed content types", () => {
render(
<div>
<Toggle label="Icon Toggle" showIcon={true} icon="I" />
<Toggle label="Text Toggle" showText={true} text="Toggle" />
<Toggle
label="Both Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>
<Toggle label="Empty Toggle" />
</div>,
);
const iconToggle = screen.getByRole("switch", { name: "Icon Toggle" });
const textToggle = screen.getByRole("switch", { name: "Text Toggle" });
const bothToggle = screen.getByRole("switch", { name: "Both Toggle" });
const emptyToggle = screen.getByRole("switch", { name: "Empty Toggle" });
expect(iconToggle).toHaveTextContent("I");
expect(textToggle).toHaveTextContent("Toggle");
expect(bothToggle).toHaveTextContent("I");
expect(bothToggle).toHaveTextContent("Toggle");
expect(emptyToggle).not.toHaveTextContent("I");
expect(emptyToggle).not.toHaveTextContent("Toggle");
});
});
@@ -1,219 +0,0 @@
import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ToggleGroup from "../../app/components/ToggleGroup";
// Test component for form integration
const TestForm = () => {
const [selectedToggle, setSelectedToggle] = useState("left");
return (
<form>
<div className="flex">
<ToggleGroup
position="left"
state={selectedToggle === "left" ? "selected" : "default"}
onChange={() => setSelectedToggle("left")}
>
Left Option
</ToggleGroup>
<ToggleGroup
position="middle"
state={selectedToggle === "middle" ? "selected" : "default"}
onChange={() => setSelectedToggle("middle")}
>
Middle Option
</ToggleGroup>
<ToggleGroup
position="right"
state={selectedToggle === "right" ? "selected" : "default"}
onChange={() => setSelectedToggle("right")}
>
Right Option
</ToggleGroup>
</div>
</form>
);
};
// Dynamic component for prop changes
const DynamicToggleGroup = ({ position, state, showText }) => {
return (
<ToggleGroup position={position} state={state} showText={showText}>
Dynamic Content
</ToggleGroup>
);
};
describe("ToggleGroup Integration", () => {
it("handles form submission", async () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<div className="flex">
<ToggleGroup position="left" onChange={() => {}}>
First Option
</ToggleGroup>
<ToggleGroup position="middle" onChange={() => {}}>
Second Option
</ToggleGroup>
<ToggleGroup position="right" onChange={() => {}}>
Third Option
</ToggleGroup>
</div>
<button type="submit">Submit</button>
</form>,
);
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
it("handles keyboard navigation between toggle groups", () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Focus first toggle group
toggleGroups[0].focus();
expect(toggleGroups[0]).toHaveFocus();
// Test keyboard navigation
fireEvent.keyDown(toggleGroups[0], { key: "Tab" });
// Note: Tab navigation behavior depends on browser implementation
});
it("handles dynamic prop changes", () => {
const { rerender } = render(
<DynamicToggleGroup position="left" state="default" showText={true} />,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
expect(toggleGroup).toHaveTextContent("Dynamic Content");
rerender(
<DynamicToggleGroup
position="middle"
state="selected"
showText={false}
/>,
);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
expect(toggleGroup).toHaveTextContent("Dynamic Content");
});
it("handles multiple toggle groups in form", () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups).toHaveLength(3);
// Test clicking different toggle groups
fireEvent.click(toggleGroups[0]);
fireEvent.click(toggleGroups[1]);
fireEvent.click(toggleGroups[2]);
});
it("handles state changes", async () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Initially, left should be selected
expect(toggleGroups[0]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
// Click middle toggle
fireEvent.click(toggleGroups[1]);
await waitFor(() => {
expect(toggleGroups[1]).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
);
});
});
it("handles content changes", () => {
const { rerender } = render(
<ToggleGroup showText={true}>Initial Content</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Initial Content");
rerender(<ToggleGroup showText={true}>Updated Content</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Updated Content");
rerender(<ToggleGroup showText={false}>Hidden Content</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Hidden Content");
});
it("handles performance with many toggle groups", () => {
const ManyToggleGroups = () => {
const [selected, setSelected] = useState(0);
return (
<div className="flex">
{Array.from({ length: 10 }, (_, i) => (
<ToggleGroup
key={i}
position={i === 0 ? "left" : i === 9 ? "right" : "middle"}
state={selected === i ? "selected" : "default"}
onChange={() => setSelected(i)}
>
Option {i + 1}
</ToggleGroup>
))}
</div>
);
};
render(<ManyToggleGroups />);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups).toHaveLength(10);
// Test clicking different toggle groups
fireEvent.click(toggleGroups[5]);
expect(toggleGroups[5]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
});
it("handles rapid state changes", async () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Rapidly change states
for (let i = 0; i < 5; i++) {
fireEvent.click(toggleGroups[i % 3]);
await waitFor(() => {
expect(toggleGroups[i % 3]).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
);
});
}
});
it("handles mixed content types", () => {
render(
<div className="flex">
<ToggleGroup position="left" showText={true}>
Text Only
</ToggleGroup>
<ToggleGroup position="middle" showText={false}>
Icon Only
</ToggleGroup>
<ToggleGroup position="right" showText={true}>
Text Only
</ToggleGroup>
</div>,
);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups[0]).toHaveTextContent("Text Only");
expect(toggleGroups[1]).toHaveTextContent("Icon Only");
expect(toggleGroups[2]).toHaveTextContent("Text Only");
});
});
@@ -1,353 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
import NumberedCards from "../../app/components/NumberedCards";
import RuleStack from "../../app/components/RuleStack";
import FeatureGrid from "../../app/components/FeatureGrid";
import QuoteBlock from "../../app/components/QuoteBlock";
import AskOrganizer from "../../app/components/AskOrganizer";
afterEach(() => {
cleanup();
});
describe("Component Interactions Integration", () => {
test("hero banner and numbered cards work together to explain the process", () => {
const heroData = {
title: "Collaborate",
subtitle: "with clarity",
description:
"Help your community make important decisions in a way that reflects its unique values.",
ctaText: "Learn how CommunityRule works",
ctaHref: "#",
};
const numberedCardsData = {
title: "How CommunityRule works",
subtitle: "Here's a quick overview of the process, from start to finish.",
cards: [
{
text: "Document how your community makes decisions",
iconShape: "blob",
iconColor: "green",
},
{
text: "Build an operating manual for a successful community",
iconShape: "gear",
iconColor: "purple",
},
{
text: "Get a link to your manual for your group to review and evolve",
iconShape: "star",
iconColor: "orange",
},
],
};
render(
<div>
<HeroBanner {...heroData} />
<NumberedCards {...numberedCardsData} />
</div>,
);
// Hero introduces the concept
expect(
screen.getByText(/Help your community make important decisions/),
).toBeInTheDocument();
// Numbered cards explain the process
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
expect(
screen.getByText("Document how your community makes decisions"),
).toBeInTheDocument();
expect(
screen.getByText("Build an operating manual for a successful community"),
).toBeInTheDocument();
expect(
screen.getByText(
"Get a link to your manual for your group to review and evolve",
),
).toBeInTheDocument();
});
test("rule stack and feature grid complement each other", () => {
const featureGridData = {
title: "We've got your back, every step of the way",
subtitle:
"Use our toolkit to improve, document, and evolve your organization.",
};
render(
<div>
<RuleStack />
<FeatureGrid {...featureGridData} />
</div>,
);
// Rule stack shows governance options
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
expect(screen.getByText("Elected Board")).toBeInTheDocument();
expect(screen.getByText("Consensus")).toBeInTheDocument();
expect(screen.getByText("Petition")).toBeInTheDocument();
// Feature grid provides support context
expect(
screen.getByText("We've got your back, every step of the way"),
).toBeInTheDocument();
expect(
screen.getByText(
"Use our toolkit to improve, document, and evolve your organization.",
),
).toBeInTheDocument();
});
test("quote block provides social proof for the entire application", () => {
render(<QuoteBlock />);
// Quote provides credibility and social proof
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
// Should have proper attribution
expect(screen.getByText("Jo Freeman")).toBeInTheDocument();
expect(
screen.getByText("The Tyranny of Structurelessness"),
).toBeInTheDocument();
});
test("ask organizer provides help context for all components", () => {
const askOrganizerData = {
title: "Still have questions?",
subtitle: "Get answers from an experienced organizer",
buttonText: "Ask an organizer",
buttonHref: "#contact",
};
render(<AskOrganizer {...askOrganizerData} />);
// Provides help for users who need assistance
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
expect(
screen.getByText("Get answers from an experienced organizer"),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /Ask an organizer/i }),
).toBeInTheDocument();
});
test("all components maintain consistent styling and branding", () => {
render(
<div>
<HeroBanner
title="Test"
subtitle="Test"
description="Test description"
ctaText="Test CTA"
/>
<NumberedCards
title="Test Cards"
subtitle="Test subtitle"
cards={[{ text: "Test card", iconShape: "blob", iconColor: "green" }]}
/>
<RuleStack />
<FeatureGrid title="Test Features" subtitle="Test subtitle" />
<QuoteBlock />
<AskOrganizer
title="Test Help"
subtitle="Test help subtitle"
buttonText="Test Help Button"
/>
</div>,
);
// All components should render without errors
expect(screen.getAllByText("Test").length).toBeGreaterThan(0);
expect(screen.getByText("Test Cards")).toBeInTheDocument();
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
expect(screen.getByText("Test Features")).toBeInTheDocument();
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
expect(screen.getByText("Test Help")).toBeInTheDocument();
});
test("components handle data flow and prop passing correctly", () => {
const testData = {
hero: {
title: "Test Hero",
subtitle: "Test Subtitle",
description: "Test description",
ctaText: "Test CTA",
ctaHref: "/test",
},
cards: {
title: "Test Cards",
subtitle: "Test subtitle",
cards: [
{ text: "Card 1", iconShape: "blob", iconColor: "green" },
{ text: "Card 2", iconShape: "gear", iconColor: "purple" },
],
},
features: {
title: "Test Features",
subtitle: "Test features subtitle",
},
help: {
title: "Test Help",
subtitle: "Test help subtitle",
buttonText: "Test Help Button",
buttonHref: "/help",
},
};
render(
<div>
<HeroBanner {...testData.hero} />
<NumberedCards {...testData.cards} />
<FeatureGrid {...testData.features} />
<AskOrganizer {...testData.help} />
</div>,
);
// Verify all data is passed correctly
expect(screen.getByText("Test Hero")).toBeInTheDocument();
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
expect(screen.getByText("Test description")).toBeInTheDocument();
expect(
screen.getAllByRole("button", { name: "Test CTA" }).length,
).toBeGreaterThan(0);
expect(screen.getByText("Test Cards")).toBeInTheDocument();
expect(screen.getByText("Card 1")).toBeInTheDocument();
expect(screen.getByText("Card 2")).toBeInTheDocument();
expect(screen.getByText("Test Features")).toBeInTheDocument();
expect(screen.getByText("Test Help")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /Test Help Button/i }),
).toBeInTheDocument();
});
test("components work together to create a cohesive user experience", async () => {
const user = userEvent.setup();
render(
<div>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions."
ctaText="Learn more"
ctaHref="#learn"
/>
<NumberedCards
title="How it works"
subtitle="Simple steps to get started"
cards={[
{ text: "Step 1", iconShape: "blob", iconColor: "green" },
{ text: "Step 2", iconShape: "gear", iconColor: "purple" },
]}
/>
<RuleStack />
<FeatureGrid title="Features" subtitle="Everything you need" />
<QuoteBlock />
<AskOrganizer
title="Need help?"
subtitle="We're here to help"
buttonText="Contact us"
buttonHref="#contact"
/>
</div>,
);
// Test interaction flow
const learnButtons = screen.getAllByRole("button", { name: "Learn more" });
await user.click(learnButtons[0]);
const createButtons = screen.getAllByRole("button", {
name: "Create CommunityRule",
});
if (createButtons.length > 0) {
await user.click(createButtons[0]);
}
const contactButton = screen.getByRole("link", { name: /Contact us/i });
await user.click(contactButton);
// All components should remain functional
expect(screen.getByText("Collaborate")).toBeInTheDocument();
expect(screen.getByText("How it works")).toBeInTheDocument();
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
expect(screen.getByText("Features")).toBeInTheDocument();
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
expect(screen.getByText("Need help?")).toBeInTheDocument();
});
test("components handle edge cases and missing data gracefully", () => {
// Test with minimal data
render(
<div>
<HeroBanner title="Minimal Hero" />
<NumberedCards title="Minimal Cards" cards={[]} />
<FeatureGrid title="Minimal Features" />
<AskOrganizer title="Minimal Help" />
</div>,
);
// Components should render without crashing
expect(screen.getByText("Minimal Hero")).toBeInTheDocument();
expect(screen.getByText("Minimal Cards")).toBeInTheDocument();
expect(screen.getByText("Minimal Features")).toBeInTheDocument();
expect(screen.getByText("Minimal Help")).toBeInTheDocument();
});
test("components maintain accessibility when used together", () => {
render(
<div>
<HeroBanner
title="Accessible Hero"
subtitle="Accessible Subtitle"
description="Accessible description"
ctaText="Accessible CTA"
/>
<NumberedCards
title="Accessible Cards"
subtitle="Accessible subtitle"
cards={[
{ text: "Accessible card", iconShape: "blob", iconColor: "green" },
]}
/>
<RuleStack />
<FeatureGrid
title="Accessible Features"
subtitle="Accessible features subtitle"
/>
<QuoteBlock />
<AskOrganizer
title="Accessible Help"
subtitle="Accessible help subtitle"
buttonText="Accessible Help Button"
/>
</div>,
);
// Check for proper heading hierarchy
const headings = screen.getAllByRole("heading");
expect(headings.length).toBeGreaterThan(0);
// Check for proper button roles
const buttons = screen.getAllByRole("button");
buttons.forEach((button) => {
expect(button).toBeInTheDocument();
});
// Check for proper link roles
const links = screen.getAllByRole("link");
links.forEach((link) => {
expect(link).toBeInTheDocument();
});
});
});
@@ -1,246 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach } from "vitest";
import Header from "../../app/components/Header";
import Footer from "../../app/components/Footer";
afterEach(() => {
cleanup();
});
describe("Layout Integration", () => {
test("header and footer provide consistent branding", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Check that CommunityRule branding appears in both header and footer
const headerLogos = screen.getAllByAltText("CommunityRule Logo Icon");
expect(headerLogos.length).toBeGreaterThan(0);
// Footer should have the organization name
expect(screen.getByText("Media Economies Design Lab")).toBeInTheDocument();
});
test("navigation is consistent between header and footer", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header navigation items
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
.length,
).toBeGreaterThan(0);
// Footer navigation items (should be present in footer as well)
// Footer has navigation links that match header
const footerUseCasesLinks = screen.getAllByRole("link", {
name: "Use cases",
});
const footerLearnLinks = screen.getAllByRole("link", { name: "Learn" });
const footerAboutLinks = screen.getAllByRole("link", { name: "About" });
// Check that footer has these links (they may be in header too, so getAllByRole will find both)
expect(footerUseCasesLinks.length).toBeGreaterThan(0);
expect(footerLearnLinks.length).toBeGreaterThan(0);
expect(footerAboutLinks.length).toBeGreaterThan(0);
});
test("header navigation is interactive", async () => {
const user = userEvent.setup();
render(<Header />);
// Test navigation links
const useCasesLinks = screen.getAllByRole("menuitem", {
name: "Navigate to Use cases page",
});
const learnLinks = screen.getAllByRole("menuitem", {
name: "Navigate to Learn page",
});
const aboutLinks = screen.getAllByRole("menuitem", {
name: "Navigate to About page",
});
const useCasesLink = useCasesLinks[0];
const learnLink = learnLinks[0];
const aboutLink = aboutLinks[0];
expect(useCasesLink).toHaveAttribute("href", "#");
expect(learnLink).toHaveAttribute("href", "/learn");
expect(aboutLink).toHaveAttribute("href", "#");
// Test button interactions
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
});
await user.click(createRuleButtons[0]);
expect(createRuleButtons[0]).toBeInTheDocument();
});
test("footer provides contact and social information", () => {
render(<Footer />);
// Contact information
expect(screen.getByText("medlab@colorado.edu")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "medlab@colorado.edu" }),
).toHaveAttribute("href", "mailto:medlab@colorado.edu");
// Social media links
expect(
screen.getByRole("link", { name: "Follow us on Bluesky" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Follow us on GitLab" }),
).toBeInTheDocument();
// Legal links
expect(
screen.getByRole("link", { name: "Privacy Policy" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Terms of Service" }),
).toBeInTheDocument();
});
test("header provides proper authentication options", () => {
render(<Header />);
// Login button should be present
const loginButtons = screen.getAllByRole("menuitem", {
name: "Log in to your account",
});
const loginButton = loginButtons[0];
expect(loginButton).toBeInTheDocument();
// Create rule button should be present
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
});
expect(createRuleButtons.length).toBeGreaterThan(0);
});
test("layout maintains proper semantic structure", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header should have banner role
const header = screen.getByRole("banner");
expect(header).toBeInTheDocument();
// Navigation should be present
const navigation = screen.getByRole("navigation");
expect(navigation).toBeInTheDocument();
// Footer should be present
const footer = screen.getByRole("contentinfo");
expect(footer).toBeInTheDocument();
});
test("responsive design elements are present", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header should have responsive navigation elements
const headerContainer = screen.getByRole("banner");
expect(headerContainer).toBeInTheDocument();
// Footer should have responsive layout
const footerContainer = screen.getByRole("contentinfo");
expect(footerContainer).toBeInTheDocument();
});
test("all interactive elements have proper focus management", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Get all interactive elements
const buttons = screen.getAllByRole("button");
const links = screen.getAllByRole("link");
// All buttons should be focusable
buttons.forEach((button) => {
expect(button).not.toHaveAttribute("tabindex", "-1");
});
// All links should be focusable
links.forEach((link) => {
expect(link).not.toHaveAttribute("tabindex", "-1");
});
});
test("layout provides consistent user experience", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header provides main navigation
expect(screen.getByRole("navigation")).toBeInTheDocument();
// Footer provides additional navigation and contact info
expect(screen.getByText("Media Economies Design Lab")).toBeInTheDocument();
expect(screen.getByText("medlab@colorado.edu")).toBeInTheDocument();
// Both header and footer should have CommunityRule branding
const logos = screen.getAllByAltText("CommunityRule Logo Icon");
expect(logos.length).toBeGreaterThan(0);
});
test("header and footer work together for complete navigation", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Main navigation in header
const headerNav = screen.getByRole("navigation");
expect(headerNav).toBeInTheDocument();
// Additional navigation in footer
const footerLinks = screen.getAllByRole("link");
const navigationLinks = footerLinks.filter(
(link) =>
link.textContent?.includes("Use cases") ||
link.textContent?.includes("Learn") ||
link.textContent?.includes("About"),
);
expect(navigationLinks.length).toBeGreaterThan(0);
// Contact information in footer
expect(
screen.getByRole("link", { name: "medlab@colorado.edu" }),
).toBeInTheDocument();
});
});
@@ -1,136 +0,0 @@
import { within, userEvent } from "@storybook/test";
import { expect } from "@storybook/test";
// Interaction test for Default story
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test initial state
expect(checkbox).toHaveAttribute("aria-checked", "false");
// Test click interaction
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test toggle back
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Checked story
export const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test initial checked state
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test unchecking
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Standard story
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
// Test both checkboxes
expect(checkboxes).toHaveLength(2);
// Test first checkbox (unchecked)
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
// Test second checkbox (checked)
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Inverse story
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
// Test both checkboxes in inverse mode
expect(checkboxes).toHaveLength(2);
// Test first checkbox (unchecked)
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
// Test second checkbox (checked)
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
// Keyboard interaction test
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Focus the checkbox
await userEvent.tab();
expect(checkbox).toHaveFocus();
// Test Space key
await userEvent.keyboard(" ");
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test Enter key
await userEvent.keyboard("Enter");
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Accessibility interaction test
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test ARIA attributes
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked");
expect(checkbox).toHaveAttribute("tabIndex");
// Test keyboard navigation
await userEvent.tab();
expect(checkbox).toHaveFocus();
// Test activation
await userEvent.keyboard(" ");
expect(checkbox).toHaveAttribute("aria-checked", "true");
},
};
// Form integration test
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test form integration
const hiddenInput = canvas.getByRole("checkbox", { hidden: true });
expect(hiddenInput).toBeInTheDocument();
// Test checkbox interaction
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(hiddenInput).toBeChecked();
},
};
-234
View File
@@ -1,234 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("Checkbox Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:6006");
});
test("should load Checkbox stories", async ({ page }) => {
// Navigate to Checkbox stories
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Check that the stories are loaded
await expect(page.locator('[data-testid="Default"]')).toBeVisible();
await expect(page.locator('[data-testid="Checked"]')).toBeVisible();
await expect(page.locator('[data-testid="Standard"]')).toBeVisible();
await expect(page.locator('[data-testid="Inverse"]')).toBeVisible();
});
test("Default story should render correctly", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check that the checkbox is rendered
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("Checked story should render correctly", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Checked"]');
// Check that the checkbox is checked
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
});
test("Standard story should show standard mode checkboxes", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Standard"]');
// Check that multiple checkboxes are rendered
const checkboxes = page.locator('[role="checkbox"]');
await expect(checkboxes).toHaveCount(2); // Unchecked and checked
// Check that they have proper styling (standard mode)
const firstCheckbox = checkboxes.first();
await expect(firstCheckbox).toBeVisible();
});
test("Inverse story should show inverse mode checkboxes", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Inverse"]');
// Check that multiple checkboxes are rendered
const checkboxes = page.locator('[role="checkbox"]');
await expect(checkboxes).toHaveCount(2); // Unchecked and checked
// Check that they have proper styling (inverse mode)
const firstCheckbox = checkboxes.first();
await expect(firstCheckbox).toBeVisible();
});
test("should have proper controls in Controls panel", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check that controls are available
await expect(page.locator('[data-testid="control-checked"]')).toBeVisible();
await expect(page.locator('[data-testid="control-mode"]')).toBeVisible();
await expect(page.locator('[data-testid="control-state"]')).toBeVisible();
await expect(
page.locator('[data-testid="control-disabled"]'),
).toBeVisible();
await expect(page.locator('[data-testid="control-label"]')).toBeVisible();
});
test("should update when controls are changed", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Toggle checked control
await page.click('[data-testid="control-checked"]');
// Check that the checkbox is now checked
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
// Toggle back
await page.click('[data-testid="control-checked"]');
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("should change mode when mode control is changed", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Change mode to inverse
await page.selectOption('[data-testid="control-mode"]', "inverse");
// Check that the checkbox styling has changed (inverse mode)
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
// Change back to standard
await page.selectOption('[data-testid="control-mode"]', "standard");
await expect(checkbox).toBeVisible();
});
test("should show disabled state when disabled control is toggled", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Toggle disabled control
await page.click('[data-testid="control-disabled"]');
// Check that the checkbox is now disabled
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("aria-disabled", "true");
await expect(checkbox).toHaveAttribute("tabIndex", "-1");
// Toggle back
await page.click('[data-testid="control-disabled"]');
await expect(checkbox).toHaveAttribute("aria-disabled", "false");
await expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should update label when label control is changed", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Change label
await page.fill('[data-testid="control-label"]', "Custom Label");
// Check that the label has updated
await expect(page.locator("text=Custom Label")).toBeVisible();
});
test("should have proper accessibility in Storybook", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check accessibility attributes
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("role", "checkbox");
await expect(checkbox).toHaveAttribute("aria-checked");
await expect(checkbox).toHaveAttribute("tabIndex");
});
test("should support keyboard navigation in Storybook", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
const checkbox = page.locator('[role="checkbox"]').first();
// Focus the checkbox
await checkbox.focus();
await expect(checkbox).toBeFocused();
// Test keyboard activation
await checkbox.press(" ");
await expect(checkbox).toHaveAttribute("aria-checked", "true");
await checkbox.press(" ");
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("should show proper documentation", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Check that documentation is available
await expect(page.locator('[data-testid="docs-tab"]')).toBeVisible();
// Click on docs tab
await page.click('[data-testid="docs-tab"]');
// Check that documentation content is shown
await expect(page.locator("text=Checkbox")).toBeVisible();
await expect(page.locator("text=Props")).toBeVisible();
});
test("should have proper story navigation", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Test navigation between stories
const stories = ["Default", "Checked", "Standard", "Inverse"];
for (const story of stories) {
await page.click(`[data-testid="${story}"]`);
await expect(page.locator('[role="checkbox"]').first()).toBeVisible();
}
});
test("should maintain state between story switches", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Interact with checkbox
const checkbox = page.locator('[role="checkbox"]').first();
await checkbox.click();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
// Switch to another story and back
await page.click('[data-testid="Checked"]');
await page.click('[data-testid="Default"]');
// Check that the state is maintained
await expect(checkbox).toHaveAttribute("aria-checked", "true");
});
});
@@ -1,124 +0,0 @@
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should be unchecked initially
await expect(radioButton).toHaveAttribute("aria-checked", "false");
// Click to check
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Radio buttons can't be unchecked by clicking them again
// They stay checked until another radio button in the same group is selected
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
export const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should be checked initially
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Radio buttons can't be unchecked by clicking them again
// They stay checked until another radio button in the same group is selected
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// First should be unchecked
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
// Click first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// First should be unchecked
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
// Click first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Focus the radio button
await userEvent.click(radioButton);
await expect(radioButton).toHaveFocus();
// Test Space key
await userEvent.keyboard(" ");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Test Enter key
await userEvent.keyboard("Enter");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
},
};
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should have proper ARIA attributes
await expect(radioButton).toHaveAttribute("role", "radio");
await expect(radioButton).toHaveAttribute("aria-checked");
await expect(radioButton).toHaveAttribute("tabIndex", "0");
// Should be keyboard accessible
await userEvent.tab();
await expect(radioButton).toHaveFocus();
// Should have accessible name
const label = canvas.getByText("Default radio button");
await expect(label).toBeVisible();
},
};
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should have hidden input for form submission
const hiddenInput = canvas.getByRole("radio", { hidden: true });
await expect(hiddenInput).toBeInTheDocument();
// Should be included in form data
await userEvent.click(radioButton);
await expect(hiddenInput).toBeChecked();
},
};
@@ -1,177 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("RadioButton Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--default",
);
});
test("renders default story", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toBeVisible();
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("renders checked story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--checked",
);
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toBeVisible();
await expect(radioButton).toHaveAttribute("aria-checked", "true");
});
test("renders standard story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--standard",
);
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
// First should be unchecked
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("renders inverse story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--inverse",
);
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
// First should be unchecked
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("interacts with controls", async ({ page }) => {
// Test checked control
await page.check('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveAttribute("aria-checked", "true");
await page.uncheck('[data-testid="checked-control"]');
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("interacts with mode control", async ({ page }) => {
// Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
await page.selectOption('[data-testid="mode-control"]', "standard");
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
});
test("interacts with state control", async ({ page }) => {
// Test state control
await page.selectOption('[data-testid="state-control"]', "focus");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(/focus:outline/);
await page.selectOption('[data-testid="state-control"]', "hover");
await expect(radioButton).toHaveClass(/hover:outline/);
});
test("interacts with label control", async ({ page }) => {
// Test label control
await page.fill('[data-testid="label-control"]', "Custom Label");
await expect(page.locator('text="Custom Label"')).toBeVisible();
});
test("handles keyboard interaction", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await radioButton.focus();
await expect(radioButton).toBeFocused();
// Test Space key
await page.keyboard.press("Space");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Test Enter key
await page.keyboard.press("Enter");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("has proper accessibility attributes", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveAttribute("role", "radio");
await expect(radioButton).toHaveAttribute("aria-checked");
await expect(radioButton).toHaveAttribute("tabIndex", "0");
});
test("shows dot indicator when checked", async ({ page }) => {
await page.check('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
const dot = radioButton.locator("div").first();
await expect(dot).toHaveClass(/w-\[16px\]/, /h-\[16px\]/, /rounded-full/);
});
test("hides dot indicator when unchecked", async ({ page }) => {
await page.uncheck('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
const dot = radioButton.locator("div").first();
await expect(dot).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
});
test("maintains focus state", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await radioButton.focus();
await expect(radioButton).toBeFocused();
// Should maintain focus after interaction
await page.keyboard.press("Space");
await expect(radioButton).toBeFocused();
});
test("handles mouse interaction", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
// Click to check
await radioButton.click();
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Click to uncheck
await radioButton.click();
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("shows proper styling for different modes", async ({ page }) => {
// Test standard mode
await page.selectOption('[data-testid="mode-control"]', "standard");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
// Test inverse mode
await page.selectOption('[data-testid="mode-control"]', "inverse");
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
});
test("handles form submission", async ({ page }) => {
const hiddenInput = page.locator('input[type="radio"]');
await expect(hiddenInput).toBeVisible();
// Should be included in form data
await page.check('[data-testid="checked-control"]');
await expect(hiddenInput).toBeChecked();
});
});
@@ -1,183 +0,0 @@
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Should have 3 radio buttons
await expect(radioButtons).toHaveLength(3);
// First should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Second should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click first option
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// First should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click second option
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const InteractiveInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Should show initial state
await expect(canvas.getByText("Selected: option1")).toBeVisible();
// Click second option
await userEvent.click(radioButtons[1]);
await expect(canvas.getByText("Selected: option2")).toBeVisible();
// Click third option
await userEvent.click(radioButtons[2]);
await expect(canvas.getByText("Selected: option3")).toBeVisible();
},
};
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Focus first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await userEvent.tab();
await expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await userEvent.keyboard(" ");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Navigate to third radio button
await userEvent.tab();
await expect(radioButtons[2]).toHaveFocus();
// Activate with Enter
await userEvent.keyboard("Enter");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have proper ARIA attributes
await expect(radioGroup).toHaveAttribute("role", "radiogroup");
radioButtons.forEach(async (button) => {
await expect(button).toHaveAttribute("role", "radio");
await expect(button).toHaveAttribute("aria-checked");
await expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have accessible names
await expect(canvas.getByText("Option 1")).toBeVisible();
await expect(canvas.getByText("Option 2")).toBeVisible();
await expect(canvas.getByText("Option 3")).toBeVisible();
},
};
export const SingleSelectionInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Initially first should be selected
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click second option
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click third option
await userEvent.click(radioButtons[2]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
},
};
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Should have hidden inputs for form submission
const hiddenInputs = canvas.getAllByRole("radio", { hidden: true });
await expect(hiddenInputs).toHaveLength(3);
// All should have the same name
const names = await Promise.all(
hiddenInputs.map((input) => input.getAttribute("name")),
);
expect(names.every((name) => name === names[0])).toBe(true);
// Should be included in form data
await userEvent.click(radioButtons[1]);
await expect(hiddenInputs[1]).toBeChecked();
},
};
@@ -1,252 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("RadioGroup Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--default",
);
});
test("renders default story", async ({ page }) => {
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
});
test("renders standard story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--standard",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// Second option should be selected
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("renders inverse story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--inverse",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// First option should be selected
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "true");
});
test("renders interactive story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--interactive",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// Should show selected value
await expect(page.locator('text="Selected: option1"')).toBeVisible();
});
test("interacts with controls", async ({ page }) => {
// Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioButtons = page.locator('[role="radio"]');
// All radio buttons should have inverse styling
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
}
await page.selectOption('[data-testid="mode-control"]', "standard");
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
}
});
test("interacts with value control", async ({ page }) => {
// Test value control
await page.fill('[data-testid="value-control"]', "option2");
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
});
test("handles keyboard navigation", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus first radio button
await radioButtons.first().focus();
await expect(radioButtons.first()).toBeFocused();
// Navigate to second radio button
await page.keyboard.press("Tab");
await expect(radioButtons.nth(1)).toBeFocused();
// Navigate to third radio button
await page.keyboard.press("Tab");
await expect(radioButtons.nth(2)).toBeFocused();
});
test("handles keyboard activation", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus second radio button
await radioButtons.nth(1).focus();
// Activate with Space
await page.keyboard.press("Space");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Activate third radio button with Enter
await radioButtons.nth(2).focus();
await page.keyboard.press("Enter");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
});
test("handles mouse interaction", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Click second option
await radioButtons.nth(1).click();
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Click third option
await radioButtons.nth(2).click();
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
});
test("maintains single selection", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Click first option
await radioButtons.first().click();
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
// Click second option
await radioButtons.nth(1).click();
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
});
test("has proper accessibility attributes", async ({ page }) => {
const radioGroup = page.locator('[role="radiogroup"]');
const radioButtons = page.locator('[role="radio"]');
await expect(radioGroup).toHaveAttribute("role", "radiogroup");
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveAttribute("role", "radio");
await expect(radioButtons.nth(i)).toHaveAttribute("aria-checked");
await expect(radioButtons.nth(i)).toHaveAttribute("tabIndex", "0");
}
});
test("shows proper labels", async ({ page }) => {
await expect(page.locator('text="Option 1"')).toBeVisible();
await expect(page.locator('text="Option 2"')).toBeVisible();
await expect(page.locator('text="Option 3"')).toBeVisible();
});
test("handles form submission", async ({ page }) => {
const hiddenInputs = page.locator('input[type="radio"]');
await expect(hiddenInputs).toHaveCount(3);
// All should have the same name
const names = await hiddenInputs.evaluateAll((inputs) =>
inputs.map((input) => input.getAttribute("name")),
);
expect(names.every((name) => name === names[0])).toBe(true);
});
test("shows dot indicators correctly", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Initially first option should be selected
const firstDot = radioButtons.first().locator("div").first();
await expect(firstDot).toHaveClass(
/w-\[16px\]/,
/h-\[16px\]/,
/rounded-full/,
);
// Click second option
await radioButtons.nth(1).click();
// First dot should be hidden, second should be visible
const secondDot = radioButtons.nth(1).locator("div").first();
await expect(secondDot).toHaveClass(
/w-\[16px\]/,
/h-\[16px\]/,
/rounded-full/,
);
});
test("handles interactive story state changes", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--interactive",
);
// Should show initial state
await expect(page.locator('text="Selected: option1"')).toBeVisible();
// Click second option
const radioButtons = page.locator('[role="radio"]');
await radioButtons.nth(1).click();
// Should update displayed value
await expect(page.locator('text="Selected: option2"')).toBeVisible();
});
test("maintains focus state", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus first radio button
await radioButtons.first().focus();
await expect(radioButtons.first()).toBeFocused();
// Should maintain focus after interaction
await page.keyboard.press("Space");
await expect(radioButtons.first()).toBeFocused();
});
test("handles different viewport sizes", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await expect(radioGroup).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await expect(radioGroup).toBeVisible();
});
});
-298
View File
@@ -1,298 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest";
import AskOrganizer from "../../app/components/AskOrganizer";
afterEach(() => {
cleanup();
});
describe("AskOrganizer Component", () => {
test("renders with all props", () => {
render(
<AskOrganizer
title="Need help organizing?"
subtitle="Get expert guidance"
description="Our organizers can help you build better communities"
buttonText="Contact an organizer"
buttonHref="/contact"
/>,
);
expect(
screen.getByRole("heading", { name: "Need help organizing?" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Get expert guidance" }),
).toBeInTheDocument();
// The description text might not be rendered or might be different
// Just verify the component renders without error
expect(
screen.getByRole("heading", { name: "Need help organizing?" }),
).toBeInTheDocument();
// Button renders as a link when href is provided
expect(
screen.getByRole("link", {
name: "Contact an organizer - Contact an organizer for help",
}),
).toBeInTheDocument();
});
test("renders with default button text", () => {
render(<AskOrganizer title="Test" subtitle="Test" description="Test" />);
// Button renders as a link when href is provided
expect(
screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
}),
).toBeInTheDocument();
});
test("renders with custom className", () => {
render(
<AskOrganizer title="Test" subtitle="Test" className="custom-class" />,
);
const section = document.querySelector("section");
expect(section).toHaveClass("custom-class");
});
test("renders different variants", () => {
const { rerender } = render(
<AskOrganizer title="Test" subtitle="Test" variant="centered" />,
);
// Centered variant should have center alignment
const container = screen
.getByRole("region")
.querySelector('[class*="text-center"]');
expect(container).toBeInTheDocument();
rerender(
<AskOrganizer title="Test" subtitle="Test" variant="left-aligned" />,
);
// Left-aligned variant should have left alignment
const leftContainer = screen
.getByRole("region")
.querySelector('[class*="text-left"]');
expect(leftContainer).toBeInTheDocument();
});
test("renders ContentLockup with ask variant", () => {
render(
<AskOrganizer
title="Ask Title"
subtitle="Ask Subtitle"
description="Ask Description"
/>,
);
expect(
screen.getByRole("heading", { name: "Ask Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Ask Subtitle" }),
).toBeInTheDocument();
// Description might not be rendered if not provided to ContentLockup
// Just verify the component renders without error
expect(
screen.getByRole("heading", { name: "Ask Title" }),
).toBeInTheDocument();
});
test("renders button with correct props", () => {
render(
<AskOrganizer
title="Test"
subtitle="Test"
buttonText="Custom Button"
buttonHref="/custom"
/>,
);
const button = screen.getByRole("link", {
name: "Custom Button - Contact an organizer for help",
});
expect(button).toHaveAttribute("href", "/custom");
expect(button).toHaveClass("xl:!px-[var(--spacing-scale-020)]");
});
test("handles button click events", async () => {
const user = userEvent.setup();
const onContactClick = vi.fn();
render(
<AskOrganizer
title="Test"
subtitle="Test"
onContactClick={onContactClick}
/>,
);
const button = screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
});
await user.click(button);
expect(onContactClick).toHaveBeenCalledWith({
event: "contact_button_click",
component: "AskOrganizer",
variant: "centered",
buttonText: "Ask an organizer",
buttonHref: "#",
timestamp: expect.any(String),
});
});
test("applies analytics tracking", async () => {
const user = userEvent.setup();
const gtagSpy = vi.fn();
// Mock window.gtag
Object.defineProperty(window, "gtag", {
value: gtagSpy,
writable: true,
});
render(<AskOrganizer title="Test" subtitle="Test" />);
const button = screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
});
await user.click(button);
// Verify gtag was called with the expected event
expect(gtagSpy).toHaveBeenCalledWith(
"event",
"contact_button_click",
expect.objectContaining({
event_category: "engagement",
event_label: "ask_organizer",
value: 1,
}),
);
});
test("renders with proper accessibility attributes", () => {
render(
<AskOrganizer title="Test" subtitle="Test" buttonText="Custom Button" />,
);
const section = document.querySelector("section");
expect(section).toHaveAttribute(
"aria-labelledby",
"ask-organizer-headline",
);
expect(section).toHaveAttribute("tabIndex", "-1");
const button = screen.getByRole("link", {
name: "Custom Button - Contact an organizer for help",
});
expect(button).toHaveAttribute(
"aria-label",
"Custom Button - Contact an organizer for help",
);
});
test("renders with design tokens", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass(
"py-[var(--spacing-scale-032)]",
"px-[var(--spacing-scale-032)]",
);
});
test("applies responsive spacing", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass(
"md:py-[var(--spacing-scale-096)]",
"md:px-[var(--spacing-scale-064)]",
);
});
test("renders with proper semantic structure", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Check for proper heading structure
const headings = screen.getAllByRole("heading");
expect(headings).toHaveLength(2); // title and subtitle
});
test("applies variant-specific styling", () => {
const { rerender } = render(
<AskOrganizer title="Test" subtitle="Test" variant="compact" />,
);
// Compact variant should have different padding
const section = screen.getByRole("region");
expect(section).toHaveClass(
"py-[var(--spacing-scale-016)]",
"px-[var(--spacing-scale-016)]",
);
rerender(
<AskOrganizer title="Test" subtitle="Test" variant="left-aligned" />,
);
// Left-aligned variant should have left alignment
const container = section.querySelector('[class*="text-left"]');
expect(container).toBeInTheDocument();
});
test("renders button with custom styling", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const button = screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
});
expect(button).toHaveClass(
"xl:!px-[var(--spacing-scale-020)]",
"xl:!py-[var(--spacing-scale-012)]",
);
});
test("handles missing optional props gracefully", () => {
render(<AskOrganizer title="Test" />);
// Should still render the structure
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Should render default button (as link when href is provided)
expect(
screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
}),
).toBeInTheDocument();
});
test("applies responsive button container alignment", () => {
render(<AskOrganizer title="Test" subtitle="Test" variant="centered" />);
// Button renders as a link when href is provided
const buttonContainer = screen
.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
})
.closest("div");
expect(buttonContainer).toHaveClass("flex", "justify-center");
});
test("renders with proper content gap", () => {
render(<AskOrganizer title="Test" subtitle="Test" variant="compact" />);
const container = screen
.getByRole("region")
.querySelector('[class*="flex flex-col"]');
expect(container).toHaveClass("gap-[var(--spacing-scale-020)]");
});
});
-160
View File
@@ -1,160 +0,0 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Button from "../../app/components/Button";
describe("Button Component", () => {
it("renders button with default props", () => {
render(<Button>Click me</Button>);
const button = screen.getByRole("button", { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass("bg-[var(--color-surface-inverse-primary)]");
expect(button).toHaveAttribute("type", "button");
});
it("renders with custom className", () => {
const customClass = "custom-button-class";
render(<Button className={customClass}>Custom Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass(customClass);
});
it("applies variant classes correctly", () => {
const { rerender } = render(<Button variant="secondary">Secondary</Button>);
let button = screen.getByRole("button");
expect(button).toHaveClass("bg-transparent");
rerender(<Button variant="primary">Primary</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("bg-[var(--color-surface-default-primary)]");
rerender(<Button variant="outlined">Outlined</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("bg-transparent", "border-[1.5px]");
});
it("applies size classes correctly", () => {
const { rerender } = render(<Button size="small">Small</Button>);
let button = screen.getByRole("button");
expect(button).toHaveClass("px-[var(--spacing-measures-spacing-008)]");
rerender(<Button size="large">Large</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("px-[var(--spacing-scale-012)]");
rerender(<Button size="xlarge">XLarge</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("px-[var(--spacing-scale-020)]");
});
it("renders as link when href is provided", () => {
const href = "/test-page";
render(<Button href={href}>Link Button</Button>);
const link = screen.getByRole("link", { name: /link button/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", href);
});
it("renders as button when href is not provided", () => {
render(<Button>Regular Button</Button>);
expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("handles click events", () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Clickable</Button>);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("applies disabled state correctly", () => {
render(<Button disabled>Disabled Button</Button>);
const button = screen.getByRole("button");
expect(button).toBeDisabled();
expect(button).toHaveClass(
"disabled:opacity-50",
"disabled:cursor-not-allowed",
);
expect(button).toHaveAttribute("aria-disabled", "true");
expect(button).toHaveAttribute("tabIndex", "-1");
});
it("applies proper accessibility attributes", () => {
render(<Button ariaLabel="Custom label">Button</Button>);
const button = screen.getByRole("button", { name: /custom label/i });
expect(button).toHaveAttribute("aria-label", "Custom label");
});
it("applies hover effects correctly", () => {
render(<Button>Hover Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("hover:scale-[1.02]", "transition-all");
});
it("applies focus styles correctly", () => {
render(<Button>Focus Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("focus:outline-none", "focus:ring-1");
});
it("applies active styles correctly", () => {
render(<Button>Active Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("active:scale-[0.98]");
});
it("handles target and rel props for links", () => {
render(
<Button href="/test" target="_blank" rel="noopener">
External Link
</Button>,
);
const link = screen.getByRole("link");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener");
});
it("forwards additional props", () => {
render(<Button data-testid="test-button">Test Button</Button>);
const button = screen.getByTestId("test-button");
expect(button).toBeInTheDocument();
});
it("applies proper font styles for different sizes", () => {
const { rerender } = render(<Button size="xsmall">XSmall</Button>);
let button = screen.getByRole("button");
expect(button).toHaveClass("text-[10px]", "leading-[12px]");
rerender(<Button size="xlarge">XLarge</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("text-[24px]", "leading-[28px]");
});
it("applies proper border radius", () => {
render(<Button>Rounded Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("rounded-[var(--radius-measures-radius-full)]");
});
it("maintains proper tab index when not disabled", () => {
render(<Button>Tab Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("tabIndex", "0");
});
});
-166
View File
@@ -1,166 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Checkbox from "../../app/components/Checkbox";
describe("Checkbox Component", () => {
test("renders with default props", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("renders with label", () => {
render(<Checkbox label="Test checkbox" />);
expect(screen.getByText("Test checkbox")).toBeInTheDocument();
});
test("renders as checked when checked prop is true", () => {
render(<Checkbox checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
});
test("renders as unchecked when checked prop is false", () => {
render(<Checkbox checked={false} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
event: expect.any(Object),
});
});
test("calls onChange when toggled from checked to unchecked", () => {
const handleChange = vi.fn();
render(<Checkbox checked={true} onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalledWith({
checked: false,
value: undefined,
event: expect.any(Object),
});
});
test("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
// Test Space key
fireEvent.keyDown(checkbox, { key: " " });
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
event: expect.any(Object),
});
// Test Enter key
fireEvent.keyDown(checkbox, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(<Checkbox disabled={true} onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).not.toHaveBeenCalled();
});
test("applies disabled attributes when disabled", () => {
render(<Checkbox disabled={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-disabled", "true");
expect(checkbox).toHaveAttribute("tabIndex", "-1");
});
test("applies correct tabIndex when not disabled", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("renders with standard mode by default", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
});
test("renders with inverse mode", () => {
render(<Checkbox mode="inverse" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
});
test("applies custom className", () => {
render(<Checkbox className="custom-class" />);
const label = screen.getByRole("checkbox").closest("label");
expect(label).toHaveClass("custom-class");
});
test("passes through additional props", () => {
render(<Checkbox id="test-checkbox" name="test" value="test-value" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("id", "test-checkbox");
});
test("renders hidden native input for form compatibility", () => {
render(<Checkbox name="test" value="test-value" checked={true} />);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "checkbox");
expect(hiddenInput).toHaveAttribute("name", "test");
expect(hiddenInput).toBeChecked();
});
test("applies aria-label when provided", () => {
render(<Checkbox ariaLabel="Custom label" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-label", "Custom label");
});
test("prevents default on mouse down", () => {
render(<Checkbox />);
const label = screen.getByRole("checkbox").closest("label");
const mouseDownEvent = new MouseEvent("mousedown", { bubbles: true });
const preventDefaultSpy = vi.spyOn(mouseDownEvent, "preventDefault");
fireEvent(label, mouseDownEvent);
expect(preventDefaultSpy).toHaveBeenCalled();
});
test("renders checkmark SVG when checked", () => {
render(<Checkbox checked={true} />);
const svg = screen.getByRole("checkbox").querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("aria-hidden", "true");
expect(svg).toHaveAttribute("focusable", "false");
});
test("does not render checkmark SVG when unchecked", () => {
render(<Checkbox checked={false} />);
const svg = screen.getByRole("checkbox").querySelector("svg");
expect(svg).toBeInTheDocument();
// SVG should be present but checkmark should be transparent
const path = svg.querySelector("polyline");
expect(path).toHaveAttribute("stroke", "transparent");
});
});
-287
View File
@@ -1,287 +0,0 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import ContentBanner from "../../app/components/ContentBanner";
// Mock Next.js components
vi.mock("next/link", () => {
return {
default: ({ children, href, ...props }) => (
<a href={href} {...props}>
{children}
</a>
),
};
});
// Mock asset utils
vi.mock("../../lib/assetUtils", () => ({
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
ASSETS: {
CONTENT_BANNER_1: "Content_Banner_1.svg",
CONTENT_BANNER_2: "Content_Banner_2.svg",
},
}));
// Mock blog post data
const mockPost = {
slug: "test-article",
frontmatter: {
title: "Test Article Title",
description: "This is a test article description",
author: "Test Author",
date: "2025-04-15",
thumbnail: {
horizontal: "test-article-horizontal.svg",
},
banner: {
horizontal: "test-article-banner.svg",
},
},
};
describe("ContentBanner", () => {
it("renders the banner with correct structure", () => {
render(<ContentBanner post={mockPost} />);
// Check that the banner container exists - it's the first div with the specific classes
const banner = document.querySelector(
"div[class*='pt-[var(--measures-spacing-016)]']",
);
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass(
"pt-[var(--measures-spacing-016)]",
"md:pt-[var(--measures-spacing-008)]",
"lg:pt-[50px]",
"xl:pt-[112px]",
"h-[275px]",
"sm:h-[326px]",
"md:h-[224px]",
"lg:h-[358.4px]",
"xl:h-[504px]",
"relative",
"w-full",
"sm:overflow-hidden",
);
});
it("displays the background image correctly", () => {
render(<ContentBanner post={mockPost} />);
// Check for background div with correct styling
const backgroundDiv = document.querySelector(
"div[style*='background-image']",
);
expect(backgroundDiv).toBeInTheDocument();
expect(backgroundDiv).toHaveClass(
"absolute",
"inset-0",
"w-full",
"h-full",
"bg-cover",
"bg-no-repeat",
"aspect-[320/225.5]",
);
});
it("shows banner image at md breakpoint and above", () => {
render(<ContentBanner post={mockPost} />);
// Check for the md+ background div with banner image
const mdBackgroundDiv = document.querySelector(
"div[style*='test-article-banner.svg']",
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("displays the article title", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
});
it("displays the article description", () => {
render(<ContentBanner post={mockPost} />);
expect(
screen.getByText("This is a test article description"),
).toBeInTheDocument();
});
it("displays the author and date metadata", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test Author")).toBeInTheDocument();
expect(screen.getByText("April 2025")).toBeInTheDocument();
});
it("applies correct styling classes", () => {
render(<ContentBanner post={mockPost} />);
// Check the content container div
const contentContainer = document.querySelector(
"div[class*='relative z-10']",
);
expect(contentContainer).toBeInTheDocument();
expect(contentContainer).toHaveClass(
"relative",
"z-10",
"h-full",
"flex",
"flex-col",
);
});
it("applies correct text styling", () => {
render(<ContentBanner post={mockPost} />);
const title = screen.getByText("Test Article Title");
expect(title).toHaveClass(
"font-bricolage",
"font-medium",
"text-[18px]",
"leading-[120%]",
"text-[var(--color-content-inverse-brand-royal)]",
);
const description = screen.getByText("This is a test article description");
expect(description).toHaveClass(
"font-inter",
"font-normal",
"text-[12px]",
"leading-[16px]",
"text-[var(--color-content-inverse-brand-royal)]",
);
});
it("applies correct metadata styling", () => {
render(<ContentBanner post={mockPost} />);
const author = screen.getByText("Test Author");
expect(author).toHaveClass(
"font-inter",
"font-normal",
"text-[10px]",
"leading-[14px]",
"text-[var(--color-content-inverse-brand-royal)]",
);
const date = screen.getByText("April 2025");
expect(date).toHaveClass(
"font-inter",
"font-normal",
"text-[10px]",
"leading-[14px]",
"text-[var(--color-content-inverse-brand-royal)]",
);
});
it("has proper spacing between elements", () => {
render(<ContentBanner post={mockPost} />);
// Check the ContentContainer spacing
const contentContainer = document.querySelector(
"div[class*='relative z-20']",
);
expect(contentContainer).toHaveClass("gap-[var(--measures-spacing-012)]");
});
it("has proper outer container padding", () => {
render(<ContentBanner post={mockPost} />);
const outerContainer = document.querySelector(
"div[class*='pt-[var(--measures-spacing-016)]']",
);
expect(outerContainer).toHaveClass(
"pt-[var(--measures-spacing-016)]",
"md:pt-[var(--measures-spacing-008)]",
"lg:pt-[50px]",
"xl:pt-[112px]",
);
});
it("handles missing post data gracefully", () => {
const incompletePost = {
slug: "incomplete",
frontmatter: {
title: "Incomplete Post",
// Missing other fields
},
};
render(<ContentBanner post={incompletePost} />);
expect(screen.getByText("Incomplete Post")).toBeInTheDocument();
});
it("falls back to thumbnail.horizontal when banner.horizontal is missing", () => {
const postWithoutBanner = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
banner: undefined,
},
};
render(<ContentBanner post={postWithoutBanner} />);
// Should use thumbnail.horizontal for md+ breakpoint
const mdBackgroundDiv = document.querySelector(
"div[style*='test-article-horizontal.svg'][class*='md:block']",
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("falls back to default banner when no images are provided", () => {
const postWithoutImages = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
thumbnail: undefined,
banner: undefined,
},
};
render(<ContentBanner post={postWithoutImages} />);
// Should use default banner for md+ breakpoint
const mdBackgroundDiv = document.querySelector(
"div[style*='Content_Banner_2.svg']",
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("applies responsive text sizing", () => {
render(<ContentBanner post={mockPost} />);
const title = screen.getByText("Test Article Title");
expect(title).toHaveClass(
"sm:text-[24px]",
"md:text-[32px]",
"lg:text-[44px]",
"xl:text-[64px]",
);
const description = screen.getByText("This is a test article description");
expect(description).toHaveClass(
"sm:text-[14px]",
"md:text-[14px]",
"lg:text-[18px]",
"xl:text-[24px]",
);
});
it("has proper accessibility attributes", () => {
render(<ContentBanner post={mockPost} />);
// Check that the component renders without accessibility errors
const banner = document.querySelector("div");
expect(banner).toBeInTheDocument();
// Check that the icon has proper alt text
const icon = screen.getByAltText("Icon for Test Article Title");
expect(icon).toBeInTheDocument();
});
});
-321
View File
@@ -1,321 +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, beforeEach } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
expect.extend(toHaveNoViolations);
describe("ContextMenu Component", () => {
const defaultProps = {
children: "Context Menu Content",
};
describe("Rendering", () => {
it("renders with default props", () => {
render(<ContextMenu {...defaultProps} />);
expect(screen.getByText("Context Menu Content")).toBeInTheDocument();
});
it("renders with custom className", () => {
render(<ContextMenu {...defaultProps} className="custom-class" />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveClass("custom-class");
});
it("applies correct base styles", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveClass(
"bg-black",
"border",
"rounded-[var(--measures-radius-medium)]",
"shadow-lg",
"p-[4px]",
);
});
it("has solid black background", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveStyle({ backgroundColor: "#000000" });
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu {...defaultProps}>
<ContextMenuItem onClick={vi.fn()}>Menu Item</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveAttribute("role", "menu");
});
});
});
describe("ContextMenuItem Component", () => {
const defaultProps = {
children: "Menu Item",
onClick: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("Rendering", () => {
it("renders with default props", () => {
render(<ContextMenuItem {...defaultProps} />);
expect(screen.getByText("Menu Item")).toBeInTheDocument();
});
it("renders as selected when selected prop is true", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
"rounded-[var(--measures-radius-small)]",
);
});
it("renders with submenu arrow when hasSubmenu prop is true", () => {
render(<ContextMenuItem {...defaultProps} hasSubmenu={true} />);
// Check for the right-pointing chevron SVG
const item = screen.getByRole("menuitem");
const svg = item.querySelector("svg:last-child");
expect(svg).toBeInTheDocument();
});
it("renders with checkmark when selected prop is true", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
// Check for the checkmark SVG
const item = screen.getByRole("menuitem");
const svg = item.querySelector("svg:first-child");
expect(svg).toBeInTheDocument();
});
it("applies correct size styles", () => {
render(<ContextMenuItem {...defaultProps} size="small" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[10px]", "leading-[14px]");
});
it("applies medium size styles", () => {
render(<ContextMenuItem {...defaultProps} size="medium" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[14px]", "leading-[20px]");
});
it("applies large size styles", () => {
render(<ContextMenuItem {...defaultProps} size="large" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[16px]", "leading-[24px]");
});
});
describe("Interaction", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByText("Menu Item");
await user.click(item);
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
});
it("does not call onClick when disabled", async () => {
const user = userEvent.setup();
render(<ContextMenuItem {...defaultProps} disabled={true} />);
const item = screen.getByText("Menu Item");
await user.click(item);
expect(defaultProps.onClick).not.toHaveBeenCalled();
});
it("has hover effects", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem {...defaultProps} />
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toBeInTheDocument();
});
});
describe("Styling", () => {
it("applies correct text color", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"text-[var(--color-content-default-brand-primary)]",
);
});
it("applies correct padding", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("px-[8px]", "py-[4px]");
});
it("applies correct gap between checkmark and text", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
const item = screen.getByText("Menu Item").closest("div");
expect(item).toHaveClass("gap-[8px]");
});
});
});
describe("ContextMenuSection Component", () => {
const defaultProps = {
title: "Section Title",
children: "Section Content",
};
describe("Rendering", () => {
it("renders with title and children", () => {
render(<ContextMenuSection {...defaultProps} />);
expect(screen.getByText("Section Title")).toBeInTheDocument();
expect(screen.getByText("Section Content")).toBeInTheDocument();
});
it("renders without title when not provided", () => {
render(<ContextMenuSection>Section Content</ContextMenuSection>);
expect(screen.getByText("Section Content")).toBeInTheDocument();
expect(screen.queryByText("Section Title")).not.toBeInTheDocument();
});
it("applies correct title styling", () => {
render(<ContextMenuSection {...defaultProps} />);
const title = screen.getByText("Section Title");
expect(title).toHaveClass(
"text-[var(--color-content-default-primary)]",
"font-medium",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuSection {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
describe("ContextMenuDivider Component", () => {
describe("Rendering", () => {
it("renders divider", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toBeInTheDocument();
});
it("applies correct styling", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-t",
"border-[var(--color-border-default-tertiary)]",
"my-1",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuDivider />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
describe("ContextMenu Components Integration", () => {
const TestMenu = () => (
<ContextMenu>
<ContextMenuSection title="First Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Second Section">
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Item 3
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
it("renders all components together", () => {
render(<TestMenu />);
expect(screen.getByText("First Section")).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
expect(screen.getByText("Second Section")).toBeInTheDocument();
expect(screen.getByText("Item 3")).toBeInTheDocument();
expect(screen.getByRole("separator")).toBeInTheDocument();
});
it("has no accessibility violations when integrated", async () => {
const { container } = render(<TestMenu />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
-144
View File
@@ -1,144 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import FeatureGrid from "../../app/components/FeatureGrid";
afterEach(() => {
cleanup();
});
describe("FeatureGrid Component", () => {
test("renders with title and subtitle", () => {
render(
<FeatureGrid
title="Feature Tools"
subtitle="Everything you need to build better communities"
/>,
);
expect(
screen.getByRole("heading", { name: "Feature Tools" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", {
name: "Everything you need to build better communities",
}),
).toBeInTheDocument();
});
test("renders with custom className", () => {
render(
<FeatureGrid title="Test" subtitle="Test" className="custom-class" />,
);
const section = document.querySelector("section");
expect(section).toHaveClass("custom-class");
});
test("renders all four MiniCard components", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
// Check for all four MiniCard components
expect(
screen.getByRole("link", { name: "Decision-making support tools" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Values alignment exercises" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Membership guidance resources" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Conflict resolution tools" }),
).toBeInTheDocument();
});
test("renders ContentLockup with feature variant", () => {
render(<FeatureGrid title="Feature Title" subtitle="Feature Subtitle" />);
expect(
screen.getByRole("heading", { name: "Feature Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Feature Subtitle" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Learn more" }),
).toBeInTheDocument();
});
test("has proper accessibility attributes", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveAttribute("aria-labelledby", "feature-grid-headline");
expect(section).toHaveAttribute("tabIndex", "-1");
const grid = screen.getByRole("grid");
expect(grid).toHaveAttribute("aria-label", "Feature tools and services");
});
test("renders with design tokens", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass("p-0", "lg:p-[var(--spacing-scale-064)]");
const container = section.querySelector('[class*="bg-[#171717]"]');
expect(container).toBeInTheDocument();
});
test("applies responsive grid layout", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const grid = screen.getByRole("grid");
expect(grid).toHaveClass("grid", "grid-cols-2", "md:grid-cols-4");
});
test("renders MiniCard with correct props", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
// Check first MiniCard (Decision-making support)
const firstCard = screen.getByRole("link", {
name: "Decision-making support tools",
});
expect(firstCard).toHaveAttribute("href", "#decision-making");
// Check second MiniCard (Values alignment)
const secondCard = screen.getByRole("link", {
name: "Values alignment exercises",
});
expect(secondCard).toHaveAttribute("href", "#values-alignment");
});
test("renders with proper semantic structure", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
const grid = screen.getByRole("grid");
expect(grid).toBeInTheDocument();
});
test("handles missing optional props gracefully", () => {
render(<FeatureGrid />);
// Should still render the structure
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Should render default MiniCards
expect(
screen.getByRole("link", { name: "Decision-making support tools" }),
).toBeInTheDocument();
});
test("applies focus-within styles", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const container = document
.querySelector("section")
.querySelector('[class*="focus-within:ring-2"]');
expect(container).toBeInTheDocument();
});
});
-285
View File
@@ -1,285 +0,0 @@
import { describe, test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import Footer from "../../app/components/Footer";
describe("Footer", () => {
test("renders footer with correct structure", () => {
render(<Footer />);
const footers = screen.getAllByRole("contentinfo");
expect(footers.length).toBeGreaterThan(0);
const footer = footers[0];
expect(footer).toBeInTheDocument();
expect(footer).toHaveClass("bg-[var(--color-surface-default-primary)]");
expect(footer).toHaveClass("w-full");
});
test("renders schema markup for organization information", () => {
render(<Footer />);
const script = document.querySelector('script[type="application/ld+json"]');
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script.textContent);
expect(schemaData["@type"]).toBe("Organization");
expect(schemaData.name).toBe("Media Economies Design Lab");
expect(schemaData.email).toBe("medlab@colorado.edu");
expect(schemaData.url).toBe("https://communityrule.com");
expect(schemaData.sameAs).toContain(
"https://bsky.app/profile/medlabboulder",
);
expect(schemaData.sameAs).toContain("https://gitlab.com/medlabboulder");
});
test("renders organization name and contact information", () => {
render(<Footer />);
expect(
screen.getAllByText("Media Economies Design Lab").length,
).toBeGreaterThan(0);
const emailLinks = screen.getAllByRole("link", {
name: "medlab@colorado.edu",
});
expect(emailLinks.length).toBeGreaterThan(0);
const emailLink = emailLinks[0];
expect(emailLink).toBeInTheDocument();
expect(emailLink).toHaveAttribute("href", "mailto:medlab@colorado.edu");
});
test("renders social media links with correct accessibility", () => {
render(<Footer />);
// Check Bluesky link
const blueskyLinks = screen.getAllByRole("link", {
name: "Follow us on Bluesky",
});
expect(blueskyLinks.length).toBeGreaterThan(0);
const blueskyLink = blueskyLinks[0];
expect(blueskyLink).toBeInTheDocument();
expect(screen.getAllByText("medlabboulder").length).toBeGreaterThan(0);
// Check GitLab link
const gitlabLinks = screen.getAllByRole("link", {
name: "Follow us on GitLab",
});
expect(gitlabLinks.length).toBeGreaterThan(0);
const gitlabLink = gitlabLinks[0];
expect(gitlabLink).toBeInTheDocument();
// Check social media images
const blueskyImages = screen.getAllByAltText("Bluesky");
expect(blueskyImages.length).toBeGreaterThan(0);
const blueskyImage = blueskyImages[0];
expect(blueskyImage).toBeInTheDocument();
expect(blueskyImage).toHaveAttribute("src", "/assets/Bluesky_Logo.svg");
const gitlabImages = screen.getAllByAltText("GitLab");
expect(gitlabImages.length).toBeGreaterThan(0);
const gitlabImage = gitlabImages[0];
expect(gitlabImage).toBeInTheDocument();
expect(gitlabImage).toHaveAttribute("src", "/assets/GitLab_Icon.png");
});
test("renders navigation links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Use cases" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Learn" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "About" }).length,
).toBeGreaterThan(0);
});
test("renders legal links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Privacy Policy" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Terms of Service" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Cookies Settings" }).length,
).toBeGreaterThan(0);
});
test("renders copyright information", () => {
render(<Footer />);
expect(screen.getAllByText("© All right reserved").length).toBeGreaterThan(
0,
);
});
test("renders responsive logo configurations", () => {
render(<Footer />);
// Check that logo containers exist for different breakpoints
const logoContainers = document.querySelectorAll(
'[class*="block sm:hidden"], [class*="hidden sm:block lg:hidden"], [class*="hidden lg:block"]',
);
expect(logoContainers.length).toBeGreaterThan(0);
});
test("has correct CSS classes for responsive design", () => {
render(<Footer />);
const footers = screen.getAllByRole("contentinfo");
expect(footers.length).toBeGreaterThan(0);
const footer = footers[0];
const mainContainer = footer.querySelector("div");
expect(mainContainer).toHaveClass("flex");
expect(mainContainer).toHaveClass("flex-col");
expect(mainContainer).toHaveClass("items-start");
expect(mainContainer).toHaveClass("mx-auto");
});
test("renders separator component", () => {
render(<Footer />);
// The Separator component should be rendered (it uses a div with border, not hr)
const separator = document.querySelector(
".bg-\\[var\\(--border-color-default-secondary\\)\\]",
);
expect(separator).toBeInTheDocument();
});
test("social media links have hover and focus states", () => {
render(<Footer />);
const blueskyLinks = screen.getAllByRole("link", {
name: "Follow us on Bluesky",
});
expect(blueskyLinks.length).toBeGreaterThan(0);
expect(blueskyLinks[0]).toHaveClass("hover:opacity-80");
expect(blueskyLinks[0]).toHaveClass("active:opacity-60");
expect(blueskyLinks[0]).toHaveClass("focus:opacity-80");
expect(blueskyLinks[0]).toHaveClass("transition-opacity");
const gitlabLinks = screen.getAllByRole("link", {
name: "Follow us on GitLab",
});
expect(gitlabLinks.length).toBeGreaterThan(0);
expect(gitlabLinks[0]).toHaveClass("hover:opacity-80");
expect(gitlabLinks[0]).toHaveClass("active:opacity-60");
expect(gitlabLinks[0]).toHaveClass("focus:opacity-80");
expect(gitlabLinks[0]).toHaveClass("transition-opacity");
});
test("navigation links have hover and focus states", () => {
render(<Footer />);
const useCasesLinks = screen.getAllByRole("link", { name: "Use cases" });
expect(useCasesLinks.length).toBeGreaterThan(0);
expect(useCasesLinks[0]).toHaveClass("hover:opacity-80");
expect(useCasesLinks[0]).toHaveClass("active:opacity-60");
expect(useCasesLinks[0]).toHaveClass("focus:opacity-80");
expect(useCasesLinks[0]).toHaveClass("transition-opacity");
});
test("legal links have hover and focus states", () => {
render(<Footer />);
const privacyLinks = screen.getAllByRole("link", {
name: "Privacy Policy",
});
expect(privacyLinks.length).toBeGreaterThan(0);
expect(privacyLinks[0]).toHaveClass("hover:opacity-80");
expect(privacyLinks[0]).toHaveClass("active:opacity-60");
expect(privacyLinks[0]).toHaveClass("focus:opacity-80");
expect(privacyLinks[0]).toHaveClass("transition-opacity");
});
test("email link has hover and focus states", () => {
render(<Footer />);
const emailLinks = screen.getAllByRole("link", {
name: "medlab@colorado.edu",
});
expect(emailLinks.length).toBeGreaterThan(0);
expect(emailLinks[0]).toHaveClass("hover:opacity-80");
expect(emailLinks[0]).toHaveClass("active:opacity-60");
expect(emailLinks[0]).toHaveClass("focus:opacity-80");
expect(emailLinks[0]).toHaveClass("transition-opacity");
});
test("social media images have hover effects", () => {
render(<Footer />);
const blueskyImages = screen.getAllByAltText("Bluesky");
expect(blueskyImages.length).toBeGreaterThan(0);
expect(blueskyImages[0]).toHaveClass("group-hover:scale-110");
expect(blueskyImages[0]).toHaveClass("transition-transform");
const gitlabImages = screen.getAllByAltText("GitLab");
expect(gitlabImages.length).toBeGreaterThan(0);
expect(gitlabImages[0]).toHaveClass("group-hover:scale-110");
expect(gitlabImages[0]).toHaveClass("transition-transform");
expect(gitlabImages[0]).toHaveClass("grayscale");
});
test("renders multiple instances of navigation links for responsive design", () => {
render(<Footer />);
// Should have navigation links in the footer
const useCasesLinks = screen.getAllByText("Use cases");
const learnLinks = screen.getAllByText("Learn");
const aboutLinks = screen.getAllByText("About");
expect(useCasesLinks.length).toBeGreaterThan(0);
expect(learnLinks.length).toBeGreaterThan(0);
expect(aboutLinks.length).toBeGreaterThan(0);
});
test("has proper focus management for accessibility", () => {
render(<Footer />);
// Get specific links that should have focus management
const emailLinks = screen.getAllByRole("link", {
name: "medlab@colorado.edu",
});
const blueskyLinks = screen.getAllByRole("link", {
name: "Follow us on Bluesky",
});
const gitlabLinks = screen.getAllByRole("link", {
name: "Follow us on GitLab",
});
// Use the first instance of each social media link
const blueskyLink = blueskyLinks[0];
const gitlabLink = gitlabLinks[0];
// Check email links (multiple due to responsive design)
emailLinks.forEach((emailLink) => {
expect(emailLink).toHaveClass("focus:outline-none");
expect(emailLink).toHaveClass("focus:ring-2");
expect(emailLink).toHaveClass("focus:ring-offset-2");
expect(emailLink).toHaveClass(
"focus:ring-[var(--color-content-default-primary)]",
);
expect(emailLink).toHaveClass(
"focus:ring-offset-[var(--color-surface-default-primary)]",
);
});
// Check social media links
[blueskyLink, gitlabLink].forEach((link) => {
expect(link).toHaveClass("focus:outline-none");
expect(link).toHaveClass("focus:ring-2");
expect(link).toHaveClass("focus:ring-offset-2");
expect(link).toHaveClass(
"focus:ring-[var(--color-content-default-primary)]",
);
expect(link).toHaveClass(
"focus:ring-offset-[var(--color-surface-default-primary)]",
);
});
});
});
-334
View File
@@ -1,334 +0,0 @@
import { describe, test, expect, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import Header, {
navigationItems,
avatarImages,
logoConfig,
} from "../../app/components/Header.js";
describe("Header", () => {
beforeEach(() => {
// Clear any existing rendered content
document.body.innerHTML = "";
});
describe("Accessibility and Landmarks", () => {
test("renders header with correct structure and accessibility attributes", () => {
const { container } = render(<Header />);
// Check main header structure - use container to scope the search
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
);
expect(header).toBeInTheDocument();
expect(header).toHaveAttribute("aria-label", "Main navigation header");
// Check navigation - use container to scope the search
const nav = container.querySelector(
'[role="navigation"][aria-label="Main navigation"]',
);
expect(nav).toBeInTheDocument();
expect(nav).toHaveAttribute("aria-label", "Main navigation");
});
test("renders all navigation items with proper accessibility", () => {
render(<Header />);
// Check all navigation items have proper aria-labels - use menuitem role since they're in a menubar
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
.length,
).toBeGreaterThan(0);
});
});
describe("Schema Markup", () => {
test("renders schema markup for site navigation", () => {
render(<Header />);
const script = document.querySelector(
'script[type="application/ld+json"]',
);
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script.textContent);
expect(schemaData["@type"]).toBe("WebSite");
expect(schemaData.name).toBe("CommunityRule");
expect(schemaData.url).toBe("https://communityrule.com");
expect(schemaData.potentialAction["@type"]).toBe("SearchAction");
});
});
describe("Configuration Data", () => {
test("navigationItems has correct structure and count", () => {
expect(navigationItems).toHaveLength(3);
expect(navigationItems[0]).toEqual({
href: "#",
text: "Use cases",
extraPadding: true,
});
expect(navigationItems[1]).toEqual({
href: "/learn",
text: "Learn",
});
expect(navigationItems[2]).toEqual({
href: "#",
text: "About",
});
});
test("avatarImages has correct structure and count", () => {
expect(avatarImages).toHaveLength(3);
expect(avatarImages[0]).toEqual({
src: "/assets/Avatar_1.png",
alt: "Avatar 1",
});
expect(avatarImages[1]).toEqual({
src: "/assets/Avatar_2.png",
alt: "Avatar 2",
});
expect(avatarImages[2]).toEqual({
src: "/assets/Avatar_3.png",
alt: "Avatar 3",
});
});
test("logoConfig has correct structure and count", () => {
expect(logoConfig).toHaveLength(5);
// Check first config (xs)
expect(logoConfig[0]).toEqual({
breakpoint: "block sm:hidden",
size: "header",
showText: false,
});
// Check last config (xl+)
expect(logoConfig[4]).toEqual({
breakpoint: "hidden xl:block",
size: "headerXl",
showText: true,
});
});
});
describe("Logo Configuration", () => {
test("renders correct number of logo variants", () => {
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
expect(logoWrappers).toHaveLength(logoConfig.length);
});
test("logo wrappers include expected breakpoint classes", () => {
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
// Check first logo variant (xs only)
expect(logoWrappers[0]).toHaveClass("block", "sm:hidden");
// Check second logo variant (sm only)
expect(logoWrappers[1]).toHaveClass("hidden", "sm:block", "md:hidden");
// Check third logo variant (md only)
expect(logoWrappers[2]).toHaveClass("hidden", "md:block", "lg:hidden");
// Check fourth logo variant (lg only)
expect(logoWrappers[3]).toHaveClass("hidden", "lg:block", "xl:hidden");
// Check fifth logo variant (xl+)
expect(logoWrappers[4]).toHaveClass("hidden", "xl:block");
});
});
describe("Navigation Structure", () => {
test("renders all breakpoint-specific navigation containers", () => {
render(<Header />);
expect(screen.getByTestId("nav-xs")).toBeInTheDocument();
expect(screen.getByTestId("nav-sm")).toBeInTheDocument();
expect(screen.getByTestId("nav-md")).toBeInTheDocument();
expect(screen.getByTestId("nav-lg")).toBeInTheDocument();
expect(screen.getByTestId("nav-xl")).toBeInTheDocument();
});
test("navigation containers use expected breakpoint classes", () => {
render(<Header />);
// XSmall navigation
const navXs = screen.getByTestId("nav-xs");
expect(navXs).toHaveClass("block", "sm:hidden");
// Small navigation
const navSm = screen.getByTestId("nav-sm");
expect(navSm).toHaveClass("hidden", "sm:block", "md:hidden");
// Medium navigation
const navMd = screen.getByTestId("nav-md");
expect(navMd).toHaveClass("hidden", "md:block", "lg:hidden");
// Large navigation
const navLg = screen.getByTestId("nav-lg");
expect(navLg).toHaveClass("hidden", "lg:block", "xl:hidden");
// XLarge navigation
const navXl = screen.getByTestId("nav-xl");
expect(navXl).toHaveClass("hidden", "xl:block");
});
test("renders navigation items with correct text and links", () => {
render(<Header />);
// Check navigation items
expect(screen.getAllByText("Use cases").length).toBeGreaterThan(0);
expect(screen.getAllByText("Learn").length).toBeGreaterThan(0);
expect(screen.getAllByText("About").length).toBeGreaterThan(0);
});
test("renders multiple instances of navigation items for responsive design", () => {
render(<Header />);
// Should have multiple instances of navigation items for different breakpoints
const useCasesLinks = screen.getAllByText("Use cases");
const learnLinks = screen.getAllByText("Learn");
const aboutLinks = screen.getAllByText("About");
expect(useCasesLinks.length).toBeGreaterThan(1);
expect(learnLinks.length).toBeGreaterThan(1);
expect(aboutLinks.length).toBeGreaterThan(1);
});
});
describe("Authentication Structure", () => {
test("renders all breakpoint-specific auth containers", () => {
render(<Header />);
expect(screen.getByTestId("auth-xs")).toBeInTheDocument();
expect(screen.getByTestId("auth-sm")).toBeInTheDocument();
expect(screen.getByTestId("auth-md")).toBeInTheDocument();
expect(screen.getByTestId("auth-lg")).toBeInTheDocument();
expect(screen.getByTestId("auth-xl")).toBeInTheDocument();
});
test("auth containers use expected breakpoint classes", () => {
render(<Header />);
// XSmall auth
const authXs = screen.getByTestId("auth-xs");
expect(authXs).toHaveClass("block", "sm:hidden");
// Small auth
const authSm = screen.getByTestId("auth-sm");
expect(authSm).toHaveClass("hidden", "sm:block", "md:hidden");
// Medium auth
const authMd = screen.getByTestId("auth-md");
expect(authMd).toHaveClass("hidden", "md:block", "lg:hidden");
// Large auth
const authLg = screen.getByTestId("auth-lg");
expect(authLg).toHaveClass("hidden", "lg:block", "xl:hidden");
// XLarge auth
const authXl = screen.getByTestId("auth-xl");
expect(authXl).toHaveClass("hidden", "xl:block");
});
test("renders login button with correct accessibility", () => {
render(<Header />);
const loginLinks = screen.getAllByRole("menuitem", {
name: "Log in to your account",
});
expect(loginLinks.length).toBeGreaterThan(0);
expect(screen.getAllByText("Log in").length).toBeGreaterThan(0);
});
test("renders multiple login buttons for responsive design", () => {
render(<Header />);
// Should have multiple login buttons for different breakpoints
const loginButtons = screen.getAllByText("Log in");
expect(loginButtons.length).toBeGreaterThan(1);
});
test("renders create rule button with avatar decoration", () => {
render(<Header />);
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
});
expect(createRuleButtons.length).toBeGreaterThan(0);
expect(screen.getAllByText("Create rule").length).toBeGreaterThan(0);
});
test("renders multiple create rule buttons for responsive design", () => {
render(<Header />);
// Should have multiple create rule buttons for different breakpoints
const createRuleButtons = screen.getAllByText("Create rule");
expect(createRuleButtons.length).toBeGreaterThan(1);
});
});
describe("Avatar Images", () => {
test("renders avatar images with correct attributes", () => {
render(<Header />);
const avatars = screen.getAllByRole("img");
expect(avatars.length).toBeGreaterThan(0);
// Check for avatar images
const avatarImages = avatars.filter(
(img) =>
img.alt === "Avatar 1" ||
img.alt === "Avatar 2" ||
img.alt === "Avatar 3",
);
expect(avatarImages.length).toBeGreaterThan(0);
});
});
describe("Sticky Header Behavior", () => {
test("applies sticky positioning classes", () => {
const { container } = render(<Header />);
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
);
expect(header).toHaveClass("sticky", "top-0", "z-50");
});
});
describe("CSS Classes and Styling", () => {
test("has correct CSS classes for styling", () => {
const { container } = render(<Header />);
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
);
expect(header).toHaveClass("bg-[var(--color-surface-default-primary)]");
expect(header).toHaveClass("w-full");
expect(header).toHaveClass("border-b");
expect(header).toHaveClass(
"border-[var(--border-color-default-tertiary)]",
);
const nav = container.querySelector(
'[role="navigation"][aria-label="Main navigation"]',
);
expect(nav).toHaveClass("flex");
expect(nav).toHaveClass("items-center");
expect(nav).toHaveClass("justify-between");
});
});
});
-141
View File
@@ -1,141 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
afterEach(() => {
cleanup();
});
describe("HeroBanner Component", () => {
test("renders with all props", () => {
render(
<HeroBanner
title="Welcome to CommunityRule"
subtitle="Build better communities"
description="Create and manage community rules with ease"
ctaText="Get Started"
ctaHref="/signup"
/>,
);
expect(
screen.getByRole("heading", { name: "Welcome to CommunityRule" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Build better communities" }),
).toBeInTheDocument();
expect(
screen.getByText("Create and manage community rules with ease"),
).toBeInTheDocument();
// Button component renders multiple versions for different screen sizes
// Use getAllByRole to handle multiple buttons with same text
const buttons = screen.getAllByRole("button", { name: "Get Started" });
expect(buttons.length).toBeGreaterThan(0);
});
test("renders with minimal props", () => {
render(<HeroBanner title="Minimal" />);
expect(
screen.getByRole("heading", { name: "Minimal" }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: "Hero illustration" }),
).toBeInTheDocument();
});
test("renders hero image", () => {
render(<HeroBanner title="Test" />);
const heroImage = screen.getByRole("img", { name: "Hero illustration" });
expect(heroImage).toBeInTheDocument();
expect(heroImage).toHaveAttribute("src", "/assets/HeroImage.png");
});
test("applies correct CSS classes", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass("bg-transparent");
// Find the div with md:flex-1 class
const contentLockup = document.querySelector('[class*="md:flex-1"]');
expect(contentLockup).toBeInTheDocument();
});
test("renders ContentLockup with correct props", () => {
render(
<HeroBanner
title="Test Title"
subtitle="Test Subtitle"
description="Test Description"
ctaText="Test CTA"
ctaHref="/test"
/>,
);
// Check that ContentLockup receives the props correctly
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Test Subtitle" }),
).toBeInTheDocument();
expect(screen.getByText("Test Description")).toBeInTheDocument();
// Button component renders multiple versions for different screen sizes
const buttons = screen.getAllByRole("button", { name: "Test CTA" });
expect(buttons.length).toBeGreaterThan(0);
});
test("renders HeroDecor component", () => {
render(<HeroBanner title="Test" />);
// HeroDecor should be present (it's a decorative component)
const heroDecor = document.querySelector(
'[class*="pointer-events-none absolute z-0"]',
);
expect(heroDecor).toBeInTheDocument();
});
test("has proper semantic structure", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Should have proper heading structure
const heading = screen.getByRole("heading", { name: "Test" });
expect(heading).toBeInTheDocument();
});
test("handles empty title gracefully", () => {
render(<HeroBanner title="" />);
// Should still render the structure even with empty title
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
test("applies responsive design classes", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass(
"px-[var(--spacing-scale-008)]",
"sm:px-[var(--spacing-scale-010)]",
);
});
test("renders with design tokens", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass("bg-transparent");
// Check for design token usage in the component structure
const container = section.querySelector(
'[class*="bg-[var(--color-surface-inverse-brand-primary)]"]',
);
expect(container).toBeInTheDocument();
});
});
-271
View File
@@ -1,271 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input";
describe("Input Component", () => {
test("renders with default props", () => {
render(<Input />);
const input = screen.getByRole("textbox");
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute("type", "text");
});
test("renders with label", () => {
render(<Input label="Test input" />);
expect(screen.getByText("Test input")).toBeInTheDocument();
expect(screen.getByLabelText("Test input")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<Input placeholder="Enter text..." />);
const input = screen.getByPlaceholderText("Enter text...");
expect(input).toBeInTheDocument();
});
test("renders with value", () => {
render(<Input value="test value" />);
const input = screen.getByDisplayValue("test value");
expect(input).toBeInTheDocument();
});
test("calls onChange when text is entered", () => {
const handleChange = vi.fn();
render(<Input onChange={handleChange} />);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "new text" } });
expect(handleChange).toHaveBeenCalledWith(expect.any(Object));
});
test("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Input onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalledWith(expect.any(Object));
});
test("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Input onBlur={handleBlur} />);
const input = screen.getByRole("textbox");
fireEvent.blur(input);
expect(handleBlur).toHaveBeenCalledWith(expect.any(Object));
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(<Input disabled={true} onChange={handleChange} />);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "new text" } });
expect(handleChange).not.toHaveBeenCalled();
});
test("does not call onFocus when disabled", () => {
const handleFocus = vi.fn();
render(<Input disabled={true} onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).not.toHaveBeenCalled();
});
test("does not call onBlur when disabled", () => {
const handleBlur = vi.fn();
render(<Input disabled={true} onBlur={handleBlur} />);
const input = screen.getByRole("textbox");
fireEvent.blur(input);
expect(handleBlur).not.toHaveBeenCalled();
});
test("applies disabled attributes when disabled", () => {
render(<Input disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toBeDisabled();
});
test("applies correct size classes", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[32px]");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[36px]");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[40px]");
});
test("applies correct label variant classes", () => {
const { rerender } = render(<Input label="Test" labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex-col");
rerender(<Input label="Test" labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center");
});
test("applies error state classes", () => {
render(<Input error={true} />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("applies disabled state classes", () => {
render(<Input disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("cursor-not-allowed");
expect(input).toHaveClass("bg-[var(--color-content-default-secondary)]");
});
test("applies focus state classes", () => {
render(<Input state="focus" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
test("applies hover state classes", () => {
render(<Input state="hover" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
test("applies active state classes", () => {
render(<Input state="active" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
});
test("applies default state classes", () => {
render(<Input state="default" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
test("applies custom className", () => {
render(<Input className="custom-class" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("custom-class");
});
test("passes through additional props", () => {
render(<Input id="test-input" name="test" type="email" />);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("id", "test-input");
expect(input).toHaveAttribute("name", "test");
expect(input).toHaveAttribute("type", "email");
});
test("generates unique ID when not provided", () => {
render(<Input label="Test" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test");
expect(input).toHaveAttribute("id");
expect(label).toHaveAttribute("for", input.id);
});
test("uses provided ID when given", () => {
render(<Input id="custom-id" label="Test" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test");
expect(input).toHaveAttribute("id", "custom-id");
expect(label).toHaveAttribute("for", "custom-id");
});
test("applies correct border radius style", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-small)");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-medium)");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-large)");
});
test("applies opacity wrapper when disabled", () => {
render(<Input disabled={true} />);
const wrapper = screen.getByRole("textbox").closest("div");
expect(wrapper).toHaveClass("opacity-40");
});
test("does not apply opacity wrapper when not disabled", () => {
render(<Input disabled={false} />);
const wrapper = screen.getByRole("textbox").closest("div");
expect(wrapper).not.toHaveClass("opacity-40");
});
test("applies correct label styling", () => {
render(<Input label="Test label" size="small" />);
const label = screen.getByText("Test label");
expect(label).toHaveClass("text-[12px]");
expect(label).toHaveClass("leading-[14px]");
expect(label).toHaveClass("font-medium");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("applies correct input text styling for different sizes", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[10px]");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[14px]");
expect(input).toHaveClass("leading-[20px]");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[16px]");
expect(input).toHaveClass("leading-[24px]");
});
test("handles keyboard navigation", () => {
const handleFocus = vi.fn();
render(<Input onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.keyDown(input, { key: "Tab" });
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalled();
});
test("forwards ref correctly", () => {
const ref = React.createRef();
render(<Input ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
test("is memoized", () => {
expect(Input.$$typeof).toBe(Symbol.for("react.memo"));
});
});
-128
View File
@@ -1,128 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Logo from "../../app/components/Logo";
describe("Logo Component", () => {
it("renders the logo with default props", () => {
render(<Logo />);
const logo = screen.getByRole("link", { name: /communityrule logo/i });
expect(logo).toBeInTheDocument();
expect(screen.getByText("CommunityRule")).toBeInTheDocument();
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
});
it("renders with custom size variant", () => {
const { rerender } = render(<Logo size="header" />);
let logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("h-[20.85px]");
rerender(<Logo size="headerLg" />);
logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("h-[28px]");
rerender(<Logo size="footer" />);
logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("h-[calc(40px*1.37)]");
});
it("renders without text when showText is false", () => {
render(<Logo showText={false} />);
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
});
it("applies proper hover effects", () => {
render(<Logo />);
const logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("hover:scale-[1.02]", "transition-all");
});
it("applies proper accessibility attributes", () => {
render(<Logo />);
const logo = screen.getByRole("link");
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
expect(logo).toHaveAttribute("href", "/");
});
it("applies proper text styling for different sizes", () => {
const { rerender } = render(<Logo size="homeHeaderMd" />);
let textElement = screen.getByText("CommunityRule");
expect(textElement).toHaveClass(
"text-[var(--color-content-inverse-primary)]",
);
rerender(<Logo size="header" />);
textElement = screen.getByText("CommunityRule");
expect(textElement).toHaveClass(
"text-[var(--color-content-default-primary)]",
);
});
it("applies proper icon sizing for different variants", () => {
const { rerender } = render(<Logo size="homeHeaderSm" />);
let icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveClass("w-[14.39px]", "h-[14.39px]");
rerender(<Logo size="headerXl" />);
icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveClass("w-[33.81px]", "h-[33.81px]");
});
it("applies brightness filter for home header variants", () => {
render(<Logo size="homeHeaderMd" />);
const icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveClass("filter", "brightness-0");
});
it("maintains proper spacing when text is hidden", () => {
render(<Logo showText={false} />);
const logo = screen.getByRole("link");
// Should not have gap class when text is hidden
expect(logo.className).not.toContain("gap-[8.28px]");
});
it("applies proper font classes to text", () => {
render(<Logo />);
const textElement = screen.getByText("CommunityRule");
expect(textElement).toHaveClass("font-bricolage-grotesque", "font-normal");
});
it("applies proper icon attributes", () => {
render(<Logo />);
const icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveAttribute("src", "/assets/Logo.svg");
expect(icon).toHaveAttribute("aria-hidden", "true");
});
it("handles all size variants correctly", () => {
const sizes = [
"default",
"homeHeaderXsmall",
"homeHeaderSm",
"homeHeaderMd",
"homeHeaderLg",
"homeHeaderXl",
"header",
"headerMd",
"headerLg",
"headerXl",
"footer",
"footerLg",
];
sizes.forEach((size) => {
const { unmount } = render(<Logo size={size} />);
const logo = screen.getByRole("link");
expect(logo).toBeInTheDocument();
unmount();
});
});
});
-248
View File
@@ -1,248 +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", () => {
it("renders with default props", () => {
render(<RadioButton />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toBeInTheDocument();
expect(radioButton).toHaveAttribute("aria-checked", "false");
});
it("renders with label", () => {
render(<RadioButton label="Test Radio" />);
expect(screen.getByText("Test Radio")).toBeInTheDocument();
});
it("shows checked state", () => {
render(<RadioButton checked={true} label="Checked Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("calls onChange when clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("calls onChange with value when clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
value="test-value"
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: "test-value",
});
});
it("does not call onChange when clicking already checked radio button", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton checked={true} onChange={handleChange} label="Test Radio" />,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
// Radio buttons should not be unchecked by clicking them again
expect(handleChange).not.toHaveBeenCalled();
});
it("handles keyboard activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton); // Focus the element first
await user.keyboard("{Enter}");
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("applies standard mode classes", () => {
render(<RadioButton mode="standard" label="Standard Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
it("applies inverse mode classes", () => {
render(<RadioButton mode="inverse" label="Inverse Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
it("applies focus state classes", () => {
render(<RadioButton state="focus" label="Focus Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass("focus:outline");
});
it("applies hover state classes", () => {
render(<RadioButton state="hover" label="Hover Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass("hover:outline");
});
it("renders hidden input for form submission", () => {
render(
<RadioButton
name="test-radio"
value="test-value"
checked={true}
label="Test Radio"
/>,
);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "radio");
expect(hiddenInput).toHaveAttribute("name", "test-radio");
expect(hiddenInput).toBeChecked();
});
it("applies custom className", () => {
render(<RadioButton className="custom-class" label="Custom Radio" />);
const label = screen.getByText("Custom Radio").closest("label");
expect(label).toHaveClass("custom-class");
});
it("generates unique ID when not provided", () => {
render(<RadioButton label="Radio 1" />);
render(<RadioButton label="Radio 2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("id");
expect(radioButtons[1]).toHaveAttribute("id");
expect(radioButtons[0].id).not.toBe(radioButtons[1].id);
});
it("uses provided ID", () => {
render(<RadioButton id="custom-id" label="Custom ID Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("id", "custom-id");
});
it("associates label with radio button for accessibility", () => {
render(<RadioButton label="Accessible Radio" />);
const radioButton = screen.getByRole("radio");
const labelId = radioButton.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent("Accessible Radio");
});
it("uses aria-label when provided", () => {
render(<RadioButton ariaLabel="Custom Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
});
it("shows dot indicator when checked", () => {
render(
<RadioButton checked={true} mode="standard" label="Checked Radio" />,
);
const dot = screen.getByRole("radio").querySelector("div");
expect(dot).toHaveClass("w-[16px]", "h-[16px]", "rounded-full");
});
it("hides dot indicator when unchecked", () => {
render(
<RadioButton checked={false} mode="standard" label="Unchecked Radio" />,
);
const dot = screen.getByRole("radio").querySelector("div");
// Check if the dot has transparent background or no background color set
const computedStyle = window.getComputedStyle(dot);
const backgroundColor = computedStyle.backgroundColor;
// The dot should either be transparent or have no background color
expect(
backgroundColor === "transparent" ||
backgroundColor === "rgba(0, 0, 0, 0)" ||
backgroundColor === "",
).toBe(true);
});
});
-240
View File
@@ -1,240 +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", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("renders with default props", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons).toHaveLength(3);
});
it("renders all options", () => {
render(<RadioGroup options={defaultOptions} />);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
it("shows selected option", () => {
render(<RadioGroup options={defaultOptions} value="option2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
});
it("calls onChange when option is selected", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("updates selection when different option is clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
// Click option 3
const option3 = screen.getByText("Option 3").closest("label");
await user.click(option3);
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
await user.click(radioButtons[2]);
await user.keyboard("{Enter}");
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("applies standard mode to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} mode="standard" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
});
it("applies inverse mode to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} mode="inverse" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
});
it("applies state to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} state="focus" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass("focus:outline");
});
});
it("generates unique group name when not provided", () => {
render(<RadioGroup options={defaultOptions} />);
render(<RadioGroup options={defaultOptions} />);
const hiddenInputs = screen.getAllByRole("radio", { hidden: true });
const names = hiddenInputs.map((input) => input.getAttribute("name"));
// Should have unique names
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBeGreaterThan(1);
});
it("uses provided name for all radio buttons", () => {
render(<RadioGroup options={defaultOptions} name="test-group" />);
const hiddenInputs = screen.getAllByDisplayValue("option1");
hiddenInputs.forEach((input) => {
expect(input).toHaveAttribute("name", "test-group");
});
});
it("applies custom className to container", () => {
render(<RadioGroup options={defaultOptions} className="custom-group" />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveClass("custom-group");
});
it("passes aria-label to radiogroup", () => {
render(
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
});
it("handles empty options array", () => {
render(<RadioGroup options={[]} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.queryAllByRole("radio");
expect(radioButtons).toHaveLength(0);
});
it("handles options with ariaLabel", () => {
const optionsWithAria = [
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
];
render(<RadioGroup options={optionsWithAria} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
});
it("maintains selection state correctly", () => {
const { rerender } = render(
<RadioGroup options={defaultOptions} value="option1" />,
);
let radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
rerender(<RadioGroup options={defaultOptions} value="option3" />);
radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
});
it("does not call onChange when clicking already selected option", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option2"
onChange={handleChange}
/>,
);
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
// Should not call onChange since it's already selected
expect(handleChange).not.toHaveBeenCalled();
});
});
-395
View File
@@ -1,395 +0,0 @@
import { describe, expect, vi, beforeEach, it } from "vitest";
import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles";
// Mock Next.js components
vi.mock("next/link", () => {
return {
default: ({ children, href, ...props }) => (
<a href={href} {...props}>
{children}
</a>
),
};
});
// Mock ContentThumbnailTemplate
vi.mock("../../app/components/ContentThumbnailTemplate", () => {
return {
default: ({ post }) => (
<div data-testid={`thumbnail-${post.slug}`}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter.title}</h3>
<p>{post.frontmatter.description}</p>
</a>
</div>
),
};
});
// Mock blog post data
const mockRelatedPosts = [
{
slug: "related-article-1",
frontmatter: {
title: "Related Article 1",
description: "This is the first related article",
author: "Test Author",
date: "2025-04-10",
},
},
{
slug: "related-article-2",
frontmatter: {
title: "Related Article 2",
description: "This is the second related article",
author: "Test Author",
date: "2025-04-12",
},
},
{
slug: "related-article-3",
frontmatter: {
title: "Related Article 3",
description: "This is the third related article",
author: "Test Author",
date: "2025-04-14",
},
},
];
describe("RelatedArticles", () => {
beforeEach(() => {
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024, // Desktop width
});
});
it("renders the section with correct structure", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
expect(section).toHaveClass(
"py-[var(--spacing-scale-032)]",
"lg:py-[var(--spacing-scale-064)]",
);
});
it("displays the section heading", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toBeInTheDocument();
expect(heading).toHaveTextContent("Related Articles");
expect(heading).toHaveClass(
"text-[32px]",
"lg:text-[44px]",
"leading-[110%]",
"font-medium",
"text-[var(--color-content-inverse-primary)]",
"text-center",
);
});
it("renders all related articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-3"),
).toBeInTheDocument();
});
it("filters out the current post from related articles", () => {
const postsWithCurrent = [
...mockRelatedPosts,
{
slug: "current-article",
frontmatter: {
title: "Current Article",
description: "This is the current article",
author: "Test Author",
date: "2025-04-15",
},
},
];
render(
<RelatedArticles
relatedPosts={postsWithCurrent}
currentPostSlug="current-article"
/>,
);
// Should not render the current article
expect(
screen.queryByTestId("thumbnail-current-article"),
).not.toBeInTheDocument();
// Should still render the other related articles
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-3"),
).toBeInTheDocument();
});
it("renders nothing when no related posts", () => {
const { container } = render(
<RelatedArticles relatedPosts={[]} currentPostSlug="current-article" />,
);
expect(container.firstChild).toBeNull();
});
it("renders nothing when all posts are filtered out", () => {
const currentPostOnly = [
{
slug: "current-article",
frontmatter: {
title: "Current Article",
description: "This is the current article",
author: "Test Author",
date: "2025-04-15",
},
},
];
const { container } = render(
<RelatedArticles
relatedPosts={currentPostOnly}
currentPostSlug="current-article"
/>,
);
expect(container.firstChild).toBeNull();
});
it("has correct container styling", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const container = document.querySelector("section > div");
expect(container).toHaveClass(
"flex",
"flex-col",
"gap-[var(--spacing-scale-032)]",
"lg:gap-[51px]",
);
});
it("has correct articles container styling", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const articlesContainer = document.querySelector("section > div > div");
expect(articlesContainer).toHaveClass(
"flex",
"justify-center",
"overflow-hidden",
);
});
it("applies correct responsive behavior for desktop", () => {
// Set desktop width (must be > 1024px to be desktop, since lg breakpoint is 1024px)
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1200,
});
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const carouselContainer = document.querySelector(
"section > div > div > div",
);
expect(carouselContainer).toHaveClass(
"overflow-x-auto",
"scrollbar-hide",
"cursor-grab",
"active:cursor-grabbing",
);
});
it("applies correct responsive behavior for mobile", () => {
// Set mobile width
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 768,
});
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const carouselContainer = document.querySelector(
"section > div > div > div",
);
expect(carouselContainer).toHaveClass(
"transition-transform",
"duration-500",
"ease-in-out",
);
});
it("handles single related article", () => {
const singlePost = [mockRelatedPosts[0]];
render(
<RelatedArticles
relatedPosts={singlePost}
currentPostSlug="current-article"
/>,
);
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-related-article-2"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-related-article-3"),
).not.toBeInTheDocument();
});
it("handles two related articles", () => {
const twoPosts = mockRelatedPosts.slice(0, 2);
render(
<RelatedArticles
relatedPosts={twoPosts}
currentPostSlug="current-article"
/>,
);
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-related-article-3"),
).not.toBeInTheDocument();
});
it("has proper accessibility attributes", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
it("applies correct gap between articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const carouselContainer = document.querySelector(
"section > div > div > div",
);
expect(carouselContainer).toHaveClass("gap-0");
});
it("handles missing currentPostSlug gracefully", () => {
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// Should still render all articles
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-3"),
).toBeInTheDocument();
});
it("handles malformed post data gracefully", () => {
const malformedPosts = [
{
slug: "malformed-1",
frontmatter: {
title: "Malformed Post 1",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
},
{
slug: "malformed-2",
frontmatter: {
title: "Malformed Post 2",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
},
];
render(
<RelatedArticles
relatedPosts={malformedPosts}
currentPostSlug="current-article"
/>,
);
expect(screen.getByTestId("thumbnail-malformed-1")).toBeInTheDocument();
expect(screen.getByTestId("thumbnail-malformed-2")).toBeInTheDocument();
});
});
-198
View File
@@ -1,198 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import SectionHeader from "../../app/components/SectionHeader";
describe("SectionHeader Component", () => {
it("renders section header with title", () => {
render(<SectionHeader title="Test Section" />);
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
// Check for both mobile and desktop versions of the title
expect(screen.getAllByText("Test Section")).toHaveLength(2);
});
it("renders with subtitle when provided", () => {
const subtitle = "This is a test subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
expect(screen.getByText(subtitle)).toBeInTheDocument();
});
it("renders with titleLg when provided", () => {
const titleLg = "Large Title for Desktop";
render(<SectionHeader title="Test Section" titleLg={titleLg} />);
// Check for mobile title and desktop titleLg
expect(screen.getByText("Test Section")).toBeInTheDocument();
expect(screen.getByText(titleLg)).toBeInTheDocument();
});
it("applies variant classes correctly", () => {
const { rerender } = render(
<SectionHeader title="Default Header" variant="default" />,
);
let titleContainer = screen
.getByRole("heading", { level: 2 })
.closest("div");
expect(titleContainer).toHaveClass(
"lg:w-[369px]",
"lg:h-[var(--spacing-scale-120)]",
);
rerender(<SectionHeader title="Multi-line Header" variant="multi-line" />);
titleContainer = screen.getByRole("heading", { level: 2 }).closest("div");
expect(titleContainer).toHaveClass(
"lg:w-[50%]",
"lg:h-[var(--spacing-scale-120)]",
);
});
it("renders responsive title spans", () => {
render(<SectionHeader title="Test Section" />);
const mobileTitle = screen.getByText("Test Section", {
selector: "span.block.lg\\:hidden",
});
const desktopTitle = screen.getByText("Test Section", {
selector: "span.hidden.lg\\:block",
});
expect(mobileTitle).toBeInTheDocument();
expect(desktopTitle).toBeInTheDocument();
});
it("uses titleLg for desktop when provided", () => {
const titleLg = "Desktop Title";
render(<SectionHeader title="Mobile Title" titleLg={titleLg} />);
const mobileTitle = screen.getByText("Mobile Title", {
selector: "span.block.lg\\:hidden",
});
const desktopTitle = screen.getByText("Desktop Title", {
selector: "span.hidden.lg\\:block",
});
expect(mobileTitle).toBeInTheDocument();
expect(desktopTitle).toBeInTheDocument();
});
it("falls back to title for desktop when titleLg not provided", () => {
render(<SectionHeader title="Test Section" />);
const mobileTitle = screen.getByText("Test Section", {
selector: "span.block.lg\\:hidden",
});
const desktopTitle = screen.getByText("Test Section", {
selector: "span.hidden.lg\\:block",
});
expect(mobileTitle).toBeInTheDocument();
expect(desktopTitle).toBeInTheDocument();
});
it("applies proper responsive layout classes", () => {
render(<SectionHeader title="Test Section" />);
const container = screen
.getByRole("heading", { level: 2 })
.closest("div").parentElement;
expect(container).toHaveClass(
"flex",
"flex-col",
"lg:flex-row",
"lg:justify-between",
);
});
it("handles empty subtitle gracefully", () => {
render(<SectionHeader title="Test Section" subtitle="" />);
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
// Empty subtitle should not cause issues - check that the paragraph element exists
const subtitleContainer = screen
.getByRole("heading", { level: 2 })
.closest("div")
.parentElement.querySelector("p");
expect(subtitleContainer).toBeInTheDocument();
});
it("maintains proper heading structure", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveTextContent("Test Section");
expect(heading.tagName).toBe("H2");
});
it("applies proper font classes", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass("font-bricolage-grotesque", "font-bold");
});
it("applies proper text sizing", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass(
"text-[28px]",
"sm:text-[32px]",
"lg:text-[32px]",
"xl:text-[40px]",
);
});
it("applies proper line heights", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass(
"leading-[36px]",
"sm:leading-[40px]",
"lg:leading-[40px]",
"xl:leading-[52px]",
);
});
it("applies proper text colors", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass("text-[var(--color-content-default-primary)]");
});
it("applies proper subtitle styling", () => {
const subtitle = "Test Subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toHaveClass("font-inter", "font-normal");
});
it("applies proper subtitle text sizing", () => {
const subtitle = "Test Subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toHaveClass(
"text-[18px]",
"sm:text-[18px]",
"lg:text-[24px]",
"xl:text-[32px]",
);
});
it("applies proper subtitle colors", () => {
const subtitle = "Test Subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toHaveClass(
"text-[#484848]",
"sm:text-[var(--color-content-default-tertiary)]",
"lg:text-[var(--color-content-default-tertiary)]",
"xl:text-[var(--color-content-default-tertiary)]",
);
});
});
-401
View File
@@ -1,401 +0,0 @@
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", () => {
const defaultProps = {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
};
describe("Rendering", () => {
it("renders with default props", () => {
render(<Select {...defaultProps} />);
expect(screen.getByText("Test Select")).toBeInTheDocument();
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("renders without label when not provided", () => {
render(
<Select
placeholder="Select an option"
options={defaultProps.options}
/>,
);
expect(screen.queryByText("Test Select")).not.toBeInTheDocument();
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("renders with horizontal label variant", () => {
render(<Select {...defaultProps} labelVariant="horizontal" />);
const container = screen.getByText("Test Select").closest("div");
expect(container).toHaveClass("flex", "items-center");
});
it("renders with default label variant", () => {
render(<Select {...defaultProps} labelVariant="default" />);
const container = screen.getByText("Test Select").closest("div");
expect(container).toHaveClass("flex", "flex-col");
});
});
describe("Size Variants", () => {
it("renders small size correctly", () => {
render(<Select {...defaultProps} size="small" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[32px]");
});
it("renders medium size correctly", () => {
render(<Select {...defaultProps} size="medium" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[36px]");
});
it("renders large size correctly", () => {
render(<Select {...defaultProps} size="large" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[40px]");
});
it("applies correct height for small horizontal label", () => {
render(
<Select {...defaultProps} size="small" labelVariant="horizontal" />,
);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[30px]");
});
it("applies correct height for small default label", () => {
render(<Select {...defaultProps} size="small" labelVariant="default" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[32px]");
});
});
describe("State Variants", () => {
it("renders default state", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
it("renders hover state", () => {
render(<Select {...defaultProps} state="hover" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
it("renders focus state", () => {
render(<Select {...defaultProps} state="focus" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("renders error state", () => {
render(<Select {...defaultProps} error={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
it("renders disabled state", () => {
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("cursor-not-allowed");
expect(selectButton).toHaveClass("opacity-40");
});
});
describe("Interaction", () => {
it("opens dropdown when clicked", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
});
it("closes dropdown when clicked again", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(selectButton);
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
it("selects an option when clicked", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<Select {...defaultProps} onChange={onChange} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(onChange).toHaveBeenCalledWith({
target: {
value: "option1",
text: "Option 1",
},
});
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
it("closes dropdown when option is selected", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
await waitFor(() => {
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
});
});
it("does not open when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
describe("Keyboard Navigation", () => {
it("opens dropdown with Enter key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("opens dropdown with Space key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard(" ");
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("closes dropdown with Escape key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
it("does not respond to keyboard when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
describe("Click Outside", () => {
it("closes dropdown when clicking outside", async () => {
const user = userEvent.setup();
render(
<div>
<Select {...defaultProps} />
<div data-testid="outside">Outside element</div>
</div>,
);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByTestId("outside"));
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
});
describe("Value Display", () => {
it("shows placeholder when no value selected", () => {
render(<Select {...defaultProps} />);
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("shows selected value when option is selected", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.queryByText("Select an option")).not.toBeInTheDocument();
});
it("shows selected value when value prop is provided", () => {
render(<Select {...defaultProps} value="option2" />);
expect(screen.getByText("Option 2")).toBeInTheDocument();
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<Select {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper ARIA attributes", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAttribute("aria-expanded", "false");
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
});
it("updates aria-expanded when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveAttribute("aria-expanded", "true");
});
});
it("associates label with select button", () => {
render(<Select {...defaultProps} />);
const label = screen.getByText("Test Select");
const selectButton = screen.getByRole("button");
expect(label).toHaveAttribute("for", selectButton.id);
});
});
describe("Focus Behavior", () => {
it("enters focus state when tabbed to", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.tab();
expect(selectButton).toHaveFocus();
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
it("does not enter focus state when clicked", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
expect(selectButton).toHaveFocus();
// Focus state should not be applied on click, only on keyboard navigation
});
});
});
-184
View File
@@ -1,184 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Switch from "../../app/components/Switch";
describe("Switch Component", () => {
it("renders with default props", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toBeInTheDocument();
expect(switchButton).toHaveAttribute("aria-checked", "false");
});
it("renders with custom props", () => {
const handleChange = vi.fn();
render(
<Switch
checked={true}
onChange={handleChange}
label="Test Switch"
state="focus"
/>,
);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
expect(screen.getByText("Test Switch")).toBeInTheDocument();
});
it("handles checked prop correctly", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles state prop correctly", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
fireEvent.click(switchButton);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Switch onBlur={handleBlur} />);
const switchButton = screen.getByRole("switch");
fireEvent.blur(switchButton);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(switchButton, { key: "Tab" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
});
it("applies correct track styles based on checked state", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
let track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
});
it("applies correct focus styles", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("applies correct base classes", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass(
"relative",
"inline-flex",
"items-center",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<Switch ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<Switch className="custom-class" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("custom-class");
});
it("renders label when provided", () => {
render(<Switch label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
});
it("does not render label when not provided", () => {
render(<Switch />);
expect(screen.queryByText("Switch label")).not.toBeInTheDocument();
// Should have aria-label for accessibility
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-label", "Toggle switch");
});
it("applies correct label styles", () => {
render(<Switch label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass(
"ml-[var(--measures-spacing-008)]",
"font-inter",
"font-normal",
"text-[14px]",
"leading-[20px]",
"text-[var(--color-content-default-primary)]",
);
});
});
-203
View File
@@ -1,203 +0,0 @@
import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
describe("TextArea", () => {
test("renders with default props", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("renders with label", () => {
render(<TextArea label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByLabelText("Test Label")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<TextArea placeholder="Enter text..." />);
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument();
});
test("renders with value", () => {
render(<TextArea value="Test value" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveValue("Test value");
});
test("renders with different sizes", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("renders with horizontal label variant", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("renders with default label variant", () => {
render(<TextArea labelVariant="default" label="Default Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
});
test("applies disabled state", () => {
render(<TextArea disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("applies error state", () => {
render(<TextArea error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("applies different states", () => {
const { rerender } = render(<TextArea state="active" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<TextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
rerender(<TextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]",
);
});
test("calls onChange when text changes", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).toHaveBeenCalledTimes(4);
});
test("calls onFocus when focused", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
render(<TextArea onFocus={handleFocus} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
});
test("calls onBlur when blurred", async () => {
const user = userEvent.setup();
const handleBlur = vi.fn();
render(<TextArea onBlur={handleBlur} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("does not call onChange when disabled", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea disabled onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).not.toHaveBeenCalled();
});
test("applies custom className", () => {
render(<TextArea className="custom-class" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("custom-class");
});
test("forwards ref", () => {
const ref = vi.fn();
render(<TextArea ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies correct height for small horizontal label", () => {
render(
<TextArea
size="small"
labelVariant="horizontal"
label="Small Horizontal"
/>,
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
});
test("applies correct height for medium horizontal label", () => {
render(
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
/>,
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[110px]");
});
test("applies correct border radius for different sizes", () => {
const { rerender } = render(<TextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-small)",
});
});
test("applies correct text color", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
});
test("applies correct label color", () => {
render(<TextArea label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
});
-195
View File
@@ -1,195 +0,0 @@
import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Toggle from "../../app/components/Toggle";
describe("Toggle Component", () => {
test("renders with default props", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
const label = screen.getByText("Test Toggle");
expect(toggle).toBeInTheDocument();
expect(label).toBeInTheDocument();
expect(toggle).toHaveAttribute("type", "button");
});
test("renders with custom props", () => {
render(
<Toggle
label="Custom Toggle"
checked={true}
disabled={true}
className="custom-class"
/>,
);
const toggle = screen.getByRole("switch");
expect(toggle).toBeInTheDocument();
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle).toHaveAttribute("disabled");
});
test("handles checked state", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
});
test("handles disabled state", () => {
render(<Toggle label="Test Toggle" disabled={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
expect(toggle).toHaveClass("cursor-not-allowed");
});
test("handles state prop", () => {
const { rerender } = render(<Toggle label="Test Toggle" state="focus" />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Toggle label="Test Toggle" state="default" />);
toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
test("handles showIcon and icon props", () => {
render(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
});
test("handles showText and text props", () => {
render(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("Toggle");
});
test("handles both icon and text", () => {
render(
<Toggle
label="Test Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>,
);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
expect(toggle).toHaveTextContent("Toggle");
});
test("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(handleChange).toHaveBeenCalledTimes(1);
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(handleChange).not.toHaveBeenCalled();
});
test("applies correct classes for different states", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-surface-default-primary)]");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<Toggle label="Test Toggle" disabled={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
});
test("applies hover classes when not checked", () => {
render(<Toggle label="Test Toggle" checked={false} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
test("does not apply hover classes when checked", () => {
render(<Toggle label="Test Toggle" checked={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
test("applies focus-visible classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
});
test("applies correct size classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("h-[var(--measures-sizing-032)]");
expect(toggle).toHaveClass("px-[16px]");
expect(toggle).toHaveClass("py-[8px]");
expect(toggle).toHaveClass("gap-[4px]");
});
test("applies correct text classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("text-[12px]");
expect(toggle).toHaveClass("leading-[16px]");
});
test("applies correct label classes", () => {
render(<Toggle label="Test Toggle" />);
const label = screen.getByText("Test Toggle");
expect(label).toHaveClass("text-[12px]");
expect(label).toHaveClass("leading-[16px]");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("forwards ref correctly", () => {
const ref = vi.fn();
render(<Toggle label="Test Toggle" ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies custom className", () => {
render(<Toggle label="Test Toggle" className="custom-class" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("custom-class");
});
});
-213
View File
@@ -1,213 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ToggleGroup from "../../app/components/ToggleGroup";
describe("ToggleGroup Component", () => {
it("renders with default props", () => {
render(<ToggleGroup>Test Content</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toBeInTheDocument();
expect(toggleGroup).toHaveTextContent("Test Content");
});
it("renders with custom props", () => {
render(
<ToggleGroup position="middle" state="selected" showText={true}>
Custom Content
</ToggleGroup>,
);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toBeInTheDocument();
expect(toggleGroup).toHaveTextContent("Custom Content");
});
it("handles position prop correctly", () => {
const { rerender } = render(
<ToggleGroup position="left">Left</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-r-[var(--measures-radius-medium)]",
"rounded-l-none",
);
});
it("handles state prop correctly", () => {
const { rerender } = render(
<ToggleGroup state="default">Default</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
);
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
"shadow-[0_0_5px_1px_#3281F8]",
);
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("handles showText prop correctly", () => {
const { rerender } = render(
<ToggleGroup showText={true}>Visible Text</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Visible Text");
rerender(<ToggleGroup showText={false}></ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("☰");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Clickable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.click(toggleGroup);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<ToggleGroup onFocus={handleFocus}>Focusable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.focus(toggleGroup);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<ToggleGroup onBlur={handleBlur}>Blurable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.blur(toggleGroup);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Keyboard</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
// Test Enter key
fireEvent.keyDown(toggleGroup, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(toggleGroup, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(toggleGroup, { key: "Escape" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(
<ToggleGroup state="default">Default</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
);
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("applies correct position classes", () => {
const { rerender } = render(
<ToggleGroup position="left">Left</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-r-[var(--measures-radius-medium)]",
"rounded-l-none",
);
});
it("applies correct base classes", () => {
render(<ToggleGroup>Base</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"py-[var(--measures-spacing-008)]",
"px-[var(--measures-spacing-008)]",
"gap-[var(--measures-spacing-008)]",
"font-inter",
"font-medium",
"text-[12px]",
"leading-[12px]",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_1px_#3281F8]",
"hover:bg-[var(--color-magenta-magenta100)]",
"flex",
"items-center",
"justify-center",
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<ToggleGroup ref={ref}>Ref Test</ToggleGroup>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<ToggleGroup className="custom-class">Custom</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("custom-class");
});
});
+221
View File
@@ -0,0 +1,221 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
type TestCases = {
renders?: boolean;
accessibility?: boolean;
keyboardNavigation?: boolean;
disabledState?: boolean;
errorState?: boolean;
};
type StateConfig<TProps> = {
disabledProps?: Partial<TProps>;
errorProps?: Partial<TProps>;
};
export interface ComponentTestSuiteConfig<TProps> {
/**
* React component under test.
*/
component: React.ComponentType<TProps>;
/**
* Human-readable name for the suite (usually the component name).
*/
name: string;
/**
* Default props used for baseline rendering.
*/
props: TProps;
/**
* Props that are considered required for the component to behave correctly.
* Used for simple sanity checks (e.g., does label text render).
*/
requiredProps?: (keyof TProps)[];
/**
* Optional props that should not cause the component to break when omitted.
*/
optionalProps?: Partial<TProps>;
/**
* Primary ARIA role for the main interactive element.
* Used for generic keyboardNavigation and accessibility checks.
*
* Examples: "button", "textbox", "checkbox", "radio", "combobox".
*/
primaryRole?: string;
/**
* Which standard tests to run for this component.
*/
testCases?: TestCases;
/**
* State-specific props for disabled/error tests.
*/
states?: StateConfig<TProps>;
}
/**
* Standardized component test suite.
*
* Usage:
* componentTestSuite({
* component: Button,
* name: "Button",
* props: { children: "Click me" },
* requiredProps: ["children"],
* primaryRole: "button",
* testCases: {
* renders: true,
* accessibility: true,
* keyboardNavigation: true,
* disabledState: true,
* },
* states: {
* disabledProps: { disabled: true },
* },
* });
*/
export function componentTestSuite<TProps>(
config: ComponentTestSuiteConfig<TProps>,
) {
const {
component: Component,
name,
props,
requiredProps = [],
optionalProps,
primaryRole = "button",
testCases = {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states = {},
} = config;
describe(`${name} (standard suite)`, () => {
if (testCases.renders) {
it("renders without crashing", () => {
render(<Component {...props} />);
});
}
if (requiredProps.length > 0) {
it("honors required props", () => {
render(<Component {...props} />);
for (const key of requiredProps) {
const value = (props as Record<string, unknown>)[key as string];
expect(
value,
`Expected required prop "${String(key)}" to be defined`,
).toBeDefined();
}
});
}
if (optionalProps) {
it("handles optional props gracefully when omitted", () => {
// Render with all props
render(<Component {...props} />);
// Render again with optional props omitted to ensure no runtime error
const { unmount } = render(
<Component {...({ ...props, ...Object.fromEntries(
Object.keys(optionalProps).map((k) => [k, undefined]),
) } as TProps)} />,
);
// Basic sanity check: component is mounted
// (we don't assert specific DOM for optional props generically)
expect(unmount).toBeDefined();
});
}
if (testCases.accessibility) {
it("has no obvious accessibility violations (axe)", async () => {
const { container } = render(<Component {...props} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
}
if (testCases.keyboardNavigation) {
it("supports basic keyboard navigation (Tab + Enter/Space)", async () => {
const user = userEvent.setup();
render(<Component {...props} />);
// Focus the primary interactive element by role
const interactive =
screen.queryByRole(primaryRole as never) ??
// Fallback: first button if specified role is not found
screen.getByRole("button");
interactive.focus();
expect(interactive).toHaveFocus();
// Trigger activation via keyboard
await user.keyboard("{Enter}");
await user.keyboard(" ");
// Still in the document after interaction
expect(interactive).toBeInTheDocument();
});
}
if (testCases.disabledState && states.disabledProps) {
it("handles disabled state correctly", async () => {
const user = userEvent.setup();
render(
<Component
{...({
...props,
...states.disabledProps,
} as TProps)}
/>,
);
const interactive =
screen.queryByRole(primaryRole as never) ??
screen.getByRole("button");
// If the component exposes disabled via attribute, assert it
if ("disabled" in interactive) {
expect(interactive).toHaveAttribute("disabled");
}
// Attempt interaction; should not throw or cause obvious change
await user.click(interactive);
expect(interactive).toBeInTheDocument();
});
}
if (testCases.errorState && states.errorProps) {
it("handles error state without crashing", () => {
// Render with error props applied; no additional assertions to keep this generic
render(
<Component
{...({
...props,
...states.errorProps,
} as TProps)}
/>,
);
});
}
});
}
-83
View File
@@ -1,83 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("Checkbox Visual Regression Tests", () => {
test("Standard mode - unchecked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-unchecked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-unchecked.png");
});
test("Standard mode - checked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-checked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-checked.png");
});
test("Inverse mode - unchecked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="inverse-unchecked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-unchecked.png");
});
test("Inverse mode - checked", async ({ page }) => {
await page.goto("/forms");
await expect(page.locator('[data-testid="inverse-checked"]')).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-checked.png");
});
test("Standard mode - hover state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="standard-unchecked"]');
await checkbox.hover();
await expect(page).toHaveScreenshot("checkbox-standard-hover.png");
});
test("Standard mode - focus state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="standard-unchecked"]');
await checkbox.focus();
await expect(page).toHaveScreenshot("checkbox-standard-focus.png");
});
test("Inverse mode - hover state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="inverse-unchecked"]');
await checkbox.hover();
await expect(page).toHaveScreenshot("checkbox-inverse-hover.png");
});
test("Inverse mode - focus state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="inverse-unchecked"]');
await checkbox.focus();
await expect(page).toHaveScreenshot("checkbox-inverse-focus.png");
});
test("Disabled state - standard", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-disabled"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-disabled.png");
});
test("Disabled state - inverse", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="inverse-disabled"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-disabled.png");
});
test("All variations grid", async ({ page }) => {
await page.goto("/forms");
await expect(page.locator('[data-testid="checkbox-grid"]')).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-all-variations.png");
});
});
-215
View File
@@ -1,215 +0,0 @@
/**
* Visual Regression Testing Configuration
*
* This file defines the configuration for visual regression testing across
* different breakpoints, components, and scenarios.
*/
// Breakpoint definitions for responsive testing
export const breakpoints = {
// Mobile breakpoints
xs: { width: 320, height: 700, name: "Extra Small" },
sm: { width: 360, height: 700, name: "Small" },
md: { width: 480, height: 700, name: "Medium" },
// Tablet breakpoints
lg: { width: 640, height: 700, name: "Large" },
xl: { width: 768, height: 700, name: "Extra Large" },
// Desktop breakpoints
"2xl": { width: 1024, height: 700, name: "2XL" },
"3xl": { width: 1280, height: 700, name: "3XL" },
"4xl": { width: 1440, height: 700, name: "4XL" },
full: { width: 1920, height: 700, name: "Full HD" },
};
// Key breakpoints for focused testing
export const keyBreakpoints = [
breakpoints.xs, // Mobile
breakpoints.md, // Tablet
breakpoints.xl, // Desktop
];
// Visual testing scenarios
export const visualScenarios = {
// Component states
states: {
default: "Default state",
hover: "Hover state",
focus: "Focus state",
active: "Active/pressed state",
disabled: "Disabled state",
},
// Interactive states
interactions: {
hover: "Element hovered",
focus: "Element focused",
click: "Element clicked",
loading: "Loading state",
error: "Error state",
},
// Content variations
content: {
short: "Short content",
long: "Long content",
empty: "Empty state",
loading: "Loading content",
error: "Error content",
},
// Layout scenarios
layout: {
compact: "Compact layout",
spacious: "Spacious layout",
stacked: "Stacked layout",
grid: "Grid layout",
list: "List layout",
},
};
// Chromatic configuration
export const chromaticConfig = {
// Viewports for Chromatic screenshots
viewports: Object.values(breakpoints).map((bp) => bp.width),
// Delay for layout stabilization
delay: 200,
// Modes for different themes
modes: {
light: {},
dark: {
colorScheme: "dark",
},
},
// Storybook viewport configuration
storybookViewports: Object.entries(breakpoints).reduce((acc, [key, bp]) => {
acc[key] = {
name: bp.name,
styles: {
width: `${bp.width}px`,
height: `${bp.height}px`,
},
};
return acc;
}, {}),
};
// Playwright visual testing configuration
export const playwrightVisualConfig = {
// Screenshot options
screenshot: {
fullPage: false,
type: "png",
quality: 90,
},
// Visual comparison options
visualComparison: {
threshold: 0.1, // 10% difference threshold
maxDiffPixels: 100,
maxDiffPixelRatio: 0.1,
},
// Test timeouts
timeouts: {
navigation: 30000,
action: 5000,
assertion: 10000,
},
};
// Component-specific visual testing configurations
export const componentConfigs = {
Header: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "authentication", "responsive"],
},
Footer: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "social", "legal"],
},
Button: {
breakpoints: [breakpoints.sm, breakpoints.md, breakpoints.lg],
states: ["default", "hover", "focus", "active", "disabled"],
variants: ["default", "home"],
sizes: ["xsmall", "small", "medium", "large", "xlarge"],
},
Logo: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover"],
variants: ["with-text", "icon-only"],
},
MenuBar: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "dropdown"],
},
};
// Visual regression test patterns
export const testPatterns = {
// Basic component testing
basic: {
description: "Basic component rendering",
steps: [
"Navigate to component",
"Wait for layout stabilization",
"Take screenshot",
],
},
// Interactive state testing
interactive: {
description: "Interactive state testing",
steps: [
"Navigate to component",
"Interact with element (hover/focus/click)",
"Wait for state change",
"Take screenshot",
],
},
// Responsive testing
responsive: {
description: "Responsive behavior testing",
steps: [
"Set viewport size",
"Navigate to component",
"Wait for layout stabilization",
"Take screenshot",
"Repeat for all breakpoints",
],
},
// Content variation testing
contentVariation: {
description: "Content variation testing",
steps: [
"Navigate to component with different content",
"Wait for layout stabilization",
"Take screenshot",
"Compare with baseline",
],
},
};
// Export all configurations
export default {
breakpoints,
keyBreakpoints,
visualScenarios,
chromaticConfig,
playwrightVisualConfig,
componentConfigs,
testPatterns,
};