Simplify and standardize testing structure
This commit is contained in:
@@ -1,396 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, describe, it, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import ContextMenu from "../../app/components/ContextMenu";
|
||||
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
||||
import ContextMenuSection from "../../app/components/ContextMenuSection";
|
||||
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("ContextMenu Components Accessibility", () => {
|
||||
describe("ContextMenu Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper role and structure", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const menu = screen.getByRole("menu");
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
const items = screen.getAllByRole("menuitem");
|
||||
expect(items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("has proper focus management", async () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const firstItem = screen.getByRole("menuitem", { name: "Item 1" });
|
||||
expect(firstItem).toHaveAttribute("tabIndex", "0");
|
||||
expect(firstItem).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenuItem Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).not.toHaveAttribute("aria-current");
|
||||
});
|
||||
|
||||
it("updates aria-current when selected", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Test Item
|
||||
</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveAttribute("aria-current", "true");
|
||||
});
|
||||
|
||||
it("is keyboard accessible", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={onClick}>Test Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
item.focus();
|
||||
|
||||
await user.keyboard("{Enter}");
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is accessible with Space key", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={onClick}>Test Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
item.focus();
|
||||
|
||||
await user.keyboard(" ");
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("has proper focus indicators", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass(
|
||||
"hover:!bg-[var(--color-surface-default-secondary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("announces selection state to screen readers", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Test Item
|
||||
</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveAttribute("aria-current", "true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenuSection Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="Test Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper heading structure", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="Test Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const title = screen.getByText("Test Section");
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has sufficient color contrast for section title", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="Test Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const title = screen.getByText("Test Section");
|
||||
expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenuDivider Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<ContextMenuDivider />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper semantic structure", () => {
|
||||
render(<ContextMenuDivider />);
|
||||
|
||||
const divider = screen.getByRole("separator");
|
||||
expect(divider).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has sufficient visual contrast", () => {
|
||||
render(<ContextMenuDivider />);
|
||||
|
||||
const divider = screen.getByRole("separator");
|
||||
expect(divider).toHaveClass(
|
||||
"border-[var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Integrated Menu Accessibility", () => {
|
||||
const TestMenu = () => (
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="First Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Item 2
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuSection title="Second Section">
|
||||
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
|
||||
Item 3
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
it("has no accessibility violations when integrated", async () => {
|
||||
const { container } = render(<TestMenu />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper menu structure", () => {
|
||||
render(<TestMenu />);
|
||||
|
||||
const menu = screen.getByRole("menu");
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
const items = screen.getAllByRole("menuitem");
|
||||
expect(items).toHaveLength(3);
|
||||
|
||||
expect(screen.getByText("First Section")).toBeInTheDocument();
|
||||
expect(screen.getByText("Second Section")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("maintains proper focus order", async () => {
|
||||
render(<TestMenu />);
|
||||
|
||||
const items = screen.getAllByRole("menuitem");
|
||||
expect(items).toHaveLength(3);
|
||||
|
||||
// Check that all items are focusable
|
||||
items.forEach((item) => {
|
||||
expect(item).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
|
||||
it("handles keyboard navigation correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={onClick}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const items = screen.getAllByRole("menuitem");
|
||||
items[0].focus();
|
||||
|
||||
await user.keyboard("{Enter}");
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Color Contrast", () => {
|
||||
it("has sufficient contrast for menu items", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass(
|
||||
"text-[var(--color-content-default-brand-primary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has sufficient contrast for section titles", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="Test Section" />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const title = screen.getByText("Test Section");
|
||||
expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
|
||||
});
|
||||
|
||||
it("has sufficient contrast for dividers", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuDivider />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const divider = screen.getByRole("separator");
|
||||
expect(divider).toHaveClass(
|
||||
"border-[var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Screen Reader Support", () => {
|
||||
it("announces menu structure correctly", () => {
|
||||
render(
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="Test Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Item 2
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
const menu = screen.getByRole("menu");
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
const items = screen.getAllByRole("menuitem");
|
||||
expect(items[0]).not.toHaveAttribute("aria-current");
|
||||
expect(items[1]).toHaveAttribute("aria-current", "true");
|
||||
});
|
||||
|
||||
it("announces selection state changes", async () => {
|
||||
const { rerender } = render(
|
||||
<ContextMenuItem onClick={vi.fn()} selected={false}>
|
||||
Test Item
|
||||
</ContextMenuItem>,
|
||||
);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).not.toHaveAttribute("aria-current");
|
||||
|
||||
rerender(
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Test Item
|
||||
</ContextMenuItem>,
|
||||
);
|
||||
|
||||
expect(item).toHaveAttribute("aria-current", "true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("WCAG Compliance", () => {
|
||||
it("meets WCAG 2.1 AA standards", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="Test Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Item 2
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 3</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("meets WCAG standards in all states", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Selected Item
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
|
||||
Submenu Item
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()} disabled={true}>
|
||||
Disabled Item
|
||||
</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,286 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Input from "../../app/components/Input";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Input Component Accessibility", () => {
|
||||
test("has no accessibility violations", async () => {
|
||||
const { container } = render(<Input label="Test input" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations when disabled", async () => {
|
||||
const { container } = render(<Input label="Test input" disabled={true} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations when in error state", async () => {
|
||||
const { container } = render(<Input label="Test input" error={true} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations with horizontal label", async () => {
|
||||
const { container } = render(
|
||||
<Input label="Test input" labelVariant="horizontal" />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("associates label with input correctly", () => {
|
||||
render(<Input label="Test input" />);
|
||||
const input = screen.getByLabelText("Test input");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute("type", "text");
|
||||
});
|
||||
|
||||
test("maintains label association with custom ID", () => {
|
||||
render(<Input id="custom-input" label="Test input" />);
|
||||
const input = screen.getByLabelText("Test input");
|
||||
expect(input).toHaveAttribute("id", "custom-input");
|
||||
});
|
||||
|
||||
test("supports keyboard navigation", () => {
|
||||
render(<Input label="Test input" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Input should be focusable
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports keyboard activation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input label="Test input" onChange={handleChange} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Type in the input
|
||||
fireEvent.change(input, { target: { value: "test" } });
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("supports Enter key activation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input label="Test input" onChange={handleChange} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Focus the input first
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
// Input should still be focused and ready for typing
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports Space key activation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input label="Test input" onChange={handleChange} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Focus the input first
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(input, { key: " " });
|
||||
// Input should still be focused and ready for typing
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports Tab navigation", () => {
|
||||
render(
|
||||
<div>
|
||||
<Input label="First input" />
|
||||
<Input label="Second input" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const firstInput = screen.getByLabelText("First input");
|
||||
const secondInput = screen.getByLabelText("Second input");
|
||||
|
||||
firstInput.focus();
|
||||
expect(firstInput).toHaveFocus();
|
||||
|
||||
// Use userEvent for more realistic tab navigation
|
||||
fireEvent.keyDown(firstInput, { key: "Tab", code: "Tab" });
|
||||
// Note: In a real browser, Tab would move focus, but in tests we need to simulate it
|
||||
secondInput.focus();
|
||||
expect(secondInput).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports Shift+Tab navigation", () => {
|
||||
render(
|
||||
<div>
|
||||
<Input label="First input" />
|
||||
<Input label="Second input" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const firstInput = screen.getByLabelText("First input");
|
||||
const secondInput = screen.getByLabelText("Second input");
|
||||
|
||||
secondInput.focus();
|
||||
expect(secondInput).toHaveFocus();
|
||||
|
||||
// Use userEvent for more realistic tab navigation
|
||||
fireEvent.keyDown(secondInput, { key: "Tab", shiftKey: true, code: "Tab" });
|
||||
// Note: In a real browser, Shift+Tab would move focus, but in tests we need to simulate it
|
||||
firstInput.focus();
|
||||
expect(firstInput).toHaveFocus();
|
||||
});
|
||||
|
||||
test("handles disabled state accessibility", () => {
|
||||
render(<Input label="Test input" disabled={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
expect(input).toHaveAttribute("disabled");
|
||||
});
|
||||
|
||||
test("handles error state accessibility", () => {
|
||||
render(<Input label="Test input" error={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
// Error state should still be accessible
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test("supports different input types", () => {
|
||||
const { rerender } = render(<Input type="email" label="Email" />);
|
||||
let input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("type", "email");
|
||||
|
||||
rerender(<Input type="password" label="Password" />);
|
||||
// Password inputs don't have textbox role, they have textbox role only for text inputs
|
||||
input = screen.getByLabelText("Password");
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
|
||||
rerender(<Input type="number" label="Number" />);
|
||||
input = screen.getByRole("spinbutton");
|
||||
expect(input).toHaveAttribute("type", "number");
|
||||
});
|
||||
|
||||
test("supports placeholder accessibility", () => {
|
||||
render(<Input placeholder="Enter your name" />);
|
||||
const input = screen.getByPlaceholderText("Enter your name");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports value accessibility", () => {
|
||||
render(<Input value="test value" />);
|
||||
const input = screen.getByDisplayValue("test value");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("maintains focus management", () => {
|
||||
const handleFocus = vi.fn();
|
||||
const handleBlur = vi.fn();
|
||||
|
||||
render(
|
||||
<Input label="Test input" onFocus={handleFocus} onBlur={handleBlur} />,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
|
||||
fireEvent.focus(input);
|
||||
expect(handleFocus).toHaveBeenCalled();
|
||||
// Focus the input to ensure it has focus
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
fireEvent.blur(input);
|
||||
expect(handleBlur).toHaveBeenCalled();
|
||||
// Manually blur the input to ensure it loses focus
|
||||
input.blur();
|
||||
expect(input).not.toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports form association", () => {
|
||||
render(
|
||||
<form>
|
||||
<Input name="test-field" label="Test input" />
|
||||
</form>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("name", "test-field");
|
||||
});
|
||||
|
||||
test("supports ARIA attributes", () => {
|
||||
render(
|
||||
<Input
|
||||
label="Test input"
|
||||
aria-describedby="help-text"
|
||||
aria-required="true"
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("aria-describedby", "help-text");
|
||||
expect(input).toHaveAttribute("aria-required", "true");
|
||||
});
|
||||
|
||||
test("supports custom ARIA labels", () => {
|
||||
render(<Input aria-label="Custom input label" />);
|
||||
const input = screen.getByLabelText("Custom input label");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles multiple inputs without conflicts", () => {
|
||||
render(
|
||||
<div>
|
||||
<Input label="First input" />
|
||||
<Input label="Second input" />
|
||||
<Input label="Third input" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const firstInput = screen.getByLabelText("First input");
|
||||
const secondInput = screen.getByLabelText("Second input");
|
||||
const thirdInput = screen.getByLabelText("Third input");
|
||||
|
||||
expect(firstInput).toBeInTheDocument();
|
||||
expect(secondInput).toBeInTheDocument();
|
||||
expect(thirdInput).toBeInTheDocument();
|
||||
|
||||
// Each should have unique IDs
|
||||
expect(firstInput.id).not.toBe(secondInput.id);
|
||||
expect(secondInput.id).not.toBe(thirdInput.id);
|
||||
expect(firstInput.id).not.toBe(thirdInput.id);
|
||||
});
|
||||
|
||||
test("supports screen reader navigation", () => {
|
||||
render(<Input label="Test input" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Test input");
|
||||
|
||||
// Label should be associated with input
|
||||
expect(label).toHaveAttribute("for", input.id);
|
||||
});
|
||||
|
||||
test("handles dynamic label changes", () => {
|
||||
const { rerender } = render(<Input label="Original label" />);
|
||||
expect(screen.getByText("Original label")).toBeInTheDocument();
|
||||
|
||||
rerender(<Input label="Updated label" />);
|
||||
expect(screen.getByText("Updated label")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Original label")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports controlled input behavior", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input value="controlled value" onChange={handleChange} />);
|
||||
|
||||
const input = screen.getByDisplayValue("controlled value");
|
||||
fireEvent.change(input, { target: { value: "new value" } });
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,306 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, describe, it, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Select from "../../app/components/Select";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Select Component Accessibility", () => {
|
||||
const defaultProps = {
|
||||
label: "Test Select",
|
||||
placeholder: "Select an option",
|
||||
options: [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
],
|
||||
};
|
||||
|
||||
describe("ARIA Attributes", () => {
|
||||
it("has correct initial ARIA attributes", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveAttribute("aria-expanded", "false");
|
||||
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
|
||||
});
|
||||
|
||||
it("updates aria-expanded when dropdown opens", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectButton).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
||||
it("has proper role for dropdown menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const listbox = screen.getByRole("listbox");
|
||||
expect(listbox).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("has proper role for menu items", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const options = screen.getAllByRole("option");
|
||||
expect(options).toHaveLength(3);
|
||||
expect(options[0]).toHaveTextContent("Option 1");
|
||||
expect(options[1]).toHaveTextContent("Option 2");
|
||||
expect(options[2]).toHaveTextContent("Option 3");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Keyboard Navigation", () => {
|
||||
it("opens dropdown with Enter key", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
selectButton.focus();
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("opens dropdown with Space key", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
selectButton.focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("closes dropdown with Escape key", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.keyboard("{Escape}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("selects option with click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<Select {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Option 1"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
target: { value: "option1", text: "Option 1" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Screen Reader Support", () => {
|
||||
it("announces selected option", async () => {
|
||||
render(<Select {...defaultProps} value="option2" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveTextContent("Option 2");
|
||||
});
|
||||
|
||||
it("announces placeholder when no option selected", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveTextContent("Select an option");
|
||||
});
|
||||
|
||||
it("has accessible name from label", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveAccessibleName("Test Select");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Focus Management", () => {
|
||||
it("maintains focus on select button when dropdown opens", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectButton).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focus to select button after selection", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Option 1"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectButton).toHaveFocus();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled State", () => {
|
||||
it("is not focusable when disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} disabled={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toBeDisabled();
|
||||
|
||||
await user.tab();
|
||||
expect(selectButton).not.toHaveFocus();
|
||||
});
|
||||
|
||||
it("has correct ARIA attributes when disabled", () => {
|
||||
render(<Select {...defaultProps} disabled={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error State", () => {
|
||||
it("announces error state to screen readers", () => {
|
||||
render(<Select {...defaultProps} error={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-negative)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WCAG Compliance", () => {
|
||||
it("meets WCAG 2.1 AA standards", async () => {
|
||||
const { container } = render(<Select {...defaultProps} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("meets WCAG standards in disabled state", async () => {
|
||||
const { container } = render(
|
||||
<Select {...defaultProps} disabled={true} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("meets WCAG standards in error state", async () => {
|
||||
const { container } = render(<Select {...defaultProps} error={true} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("meets WCAG standards when dropdown is open", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Color Contrast", () => {
|
||||
it("has sufficient color contrast for text", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"text-[var(--color-content-default-primary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has sufficient color contrast for labels", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const label = screen.getByText("Test Select");
|
||||
expect(label).toHaveClass(
|
||||
"text-[var(--color-content-default-secondary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Focus Indicators", () => {
|
||||
it("has visible focus indicator", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"focus-visible:border-[var(--color-border-default-utility-info)]",
|
||||
);
|
||||
expect(selectButton).toHaveClass(
|
||||
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
|
||||
);
|
||||
});
|
||||
|
||||
it("distinguishes between focus and hover states", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
// Focus state should be different from hover state
|
||||
expect(selectButton).toHaveClass(
|
||||
"focus-visible:border-[var(--color-border-default-utility-info)]",
|
||||
);
|
||||
expect(selectButton).toHaveClass(
|
||||
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Switch from "../../app/components/Switch";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Switch Accessibility", () => {
|
||||
it("has proper ARIA attributes", () => {
|
||||
render(<Switch checked={false} label="Test Switch" />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
|
||||
expect(switchButton).toHaveAttribute("role", "switch");
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "false");
|
||||
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes when checked", () => {
|
||||
render(<Switch checked={true} label="Test Switch" />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes when focused", () => {
|
||||
render(<Switch state="focus" label="Test Switch" />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "false");
|
||||
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
|
||||
expect(switchButton).toHaveClass("rounded-full");
|
||||
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
|
||||
});
|
||||
|
||||
it("handles keyboard navigation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Switch onChange={handleChange} label="Test Switch" />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(switchButton, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(switchButton, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("handles focus state accessibility", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<Switch onFocus={handleFocus} label="Test Switch" />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
|
||||
fireEvent.focus(switchButton);
|
||||
expect(handleFocus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles checked state accessibility", () => {
|
||||
const { rerender } = render(<Switch checked={false} label="Test Switch" />);
|
||||
let switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
rerender(<Switch checked={true} label="Test Switch" />);
|
||||
switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<Switch label="Test Switch" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when checked", async () => {
|
||||
const { container } = render(<Switch checked={true} label="Test Switch" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when focused", async () => {
|
||||
const { container } = render(<Switch state="focus" label="Test Switch" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations with text", async () => {
|
||||
const { container } = render(<Switch label="Enable notifications" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations without text", async () => {
|
||||
const { container } = render(<Switch />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import TextArea from "../../app/components/TextArea";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("TextArea Accessibility", () => {
|
||||
test("renders without accessibility violations", async () => {
|
||||
const { container } = render(<TextArea label="Test TextArea" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has proper label association", () => {
|
||||
render(<TextArea label="Test Label" />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Test Label");
|
||||
|
||||
expect(textarea).toHaveAttribute("id");
|
||||
expect(label).toHaveAttribute("for", textarea.id);
|
||||
});
|
||||
|
||||
test("has proper ARIA attributes", () => {
|
||||
render(<TextArea label="Test Label" name="test-textarea" />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
|
||||
expect(textarea).toHaveAttribute("id");
|
||||
expect(textarea).toHaveAttribute("name", "test-textarea");
|
||||
});
|
||||
|
||||
test("supports keyboard navigation", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TextArea label="Test Label" />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.tab();
|
||||
|
||||
expect(textarea).toHaveFocus();
|
||||
});
|
||||
|
||||
test("announces changes to screen readers", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
render(<TextArea label="Test Label" onChange={handleChange} />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.type(textarea, "test");
|
||||
|
||||
expect(textarea).toHaveValue("test");
|
||||
});
|
||||
|
||||
test("handles disabled state accessibility", () => {
|
||||
render(<TextArea label="Test Label" disabled />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
|
||||
expect(textarea).toBeDisabled();
|
||||
expect(textarea).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
test("handles error state accessibility", () => {
|
||||
render(<TextArea label="Test Label" error />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
|
||||
expect(textarea).toHaveAttribute("aria-invalid", "true");
|
||||
});
|
||||
|
||||
test("maintains focus management", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TextArea label="Test Label" />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.click(textarea);
|
||||
|
||||
expect(textarea).toHaveFocus();
|
||||
});
|
||||
|
||||
test("supports horizontal label layout", () => {
|
||||
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Horizontal Label");
|
||||
|
||||
expect(textarea).toBeInTheDocument();
|
||||
expect(label).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles different sizes accessibility", () => {
|
||||
const { rerender } = render(<TextArea size="small" label="Small" />);
|
||||
let textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
|
||||
rerender(<TextArea size="medium" label="Medium" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
|
||||
rerender(<TextArea size="large" label="Large" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("maintains proper contrast ratios", () => {
|
||||
render(<TextArea label="Test Label" />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Test Label");
|
||||
|
||||
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
|
||||
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
|
||||
});
|
||||
|
||||
test("supports screen reader announcements for state changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TextArea label="Test Label" />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.click(textarea);
|
||||
await user.type(textarea, "Hello");
|
||||
|
||||
expect(textarea).toHaveValue("Hello");
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Toggle from "../../app/components/Toggle";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Toggle Accessibility", () => {
|
||||
test("has proper ARIA attributes", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||
expect(toggle).toHaveAttribute("type", "button");
|
||||
});
|
||||
|
||||
test("has proper ARIA attributes when checked", () => {
|
||||
render(<Toggle label="Test Toggle" checked={true} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
test("has proper ARIA attributes when disabled", () => {
|
||||
render(<Toggle label="Test Toggle" disabled={true} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("disabled");
|
||||
});
|
||||
|
||||
test("has proper label association", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
const label = screen.getByText("Test Toggle");
|
||||
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(label).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles keyboard navigation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Toggle label="Test Toggle" onChange={handleChange} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
toggle.focus();
|
||||
expect(toggle).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(toggle, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.keyDown(toggle, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("handles disabled state accessibility", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(
|
||||
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("disabled");
|
||||
expect(toggle).toHaveClass("cursor-not-allowed");
|
||||
|
||||
fireEvent.click(toggle);
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles focus state accessibility", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
|
||||
});
|
||||
|
||||
test("has no accessibility violations", async () => {
|
||||
const { container } = render(<Toggle label="Test Toggle" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations when checked", async () => {
|
||||
const { container } = render(<Toggle label="Test Toggle" checked={true} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations when disabled", async () => {
|
||||
const { container } = render(
|
||||
<Toggle label="Test Toggle" disabled={true} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations with icon", async () => {
|
||||
const { container } = render(
|
||||
<Toggle label="Test Toggle" showIcon={true} icon="I" />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("has no accessibility violations with text", async () => {
|
||||
const { container } = render(
|
||||
<Toggle label="Test Toggle" showText={true} text="Toggle" />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import ToggleGroup from "../../app/components/ToggleGroup";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("ToggleGroup Accessibility", () => {
|
||||
it("has proper ARIA attributes", () => {
|
||||
render(<ToggleGroup>Toggle Item</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveAttribute("type", "button");
|
||||
expect(toggleGroup).toHaveAttribute("role", "button");
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes when focused", () => {
|
||||
render(<ToggleGroup state="focus">Focused Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveAttribute("type", "button");
|
||||
expect(toggleGroup).toHaveAttribute("role", "button");
|
||||
});
|
||||
|
||||
it("handles keyboard navigation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<ToggleGroup onChange={handleChange}>Keyboard Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(toggleGroup, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(toggleGroup, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("handles focus state accessibility", () => {
|
||||
render(<ToggleGroup state="focus">Focus Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
});
|
||||
|
||||
it("handles selected state accessibility", () => {
|
||||
render(<ToggleGroup state="selected">Selected Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<ToggleGroup>Accessible Toggle</ToggleGroup>);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when focused", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup state="focus">Focused Toggle</ToggleGroup>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when selected", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup state="selected">Selected Toggle</ToggleGroup>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations with text", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup showText={true}>Text Toggle</ToggleGroup>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations without text", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup showText={false} ariaLabel="Icon Toggle">
|
||||
Icon Toggle
|
||||
</ToggleGroup>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -1,158 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { expect, test, describe } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Checkbox from "../../../app/components/Checkbox";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Checkbox Accessibility", () => {
|
||||
test("should not have accessibility violations when unchecked", async () => {
|
||||
const { container } = render(<Checkbox label="Test checkbox" />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should not have accessibility violations when checked", async () => {
|
||||
const { container } = render(
|
||||
<Checkbox label="Test checkbox" checked={true} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should not have accessibility violations when disabled", async () => {
|
||||
const { container } = render(
|
||||
<Checkbox label="Test checkbox" disabled={true} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should not have accessibility violations in inverse mode", async () => {
|
||||
const { container } = render(
|
||||
<Checkbox label="Test checkbox" mode="inverse" />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should have proper ARIA attributes", () => {
|
||||
render(<Checkbox label="Test checkbox" checked={true} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).toHaveAttribute("role", "checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
test("should have proper ARIA attributes when disabled", () => {
|
||||
render(<Checkbox label="Test checkbox" disabled={true} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).toHaveAttribute("role", "checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
||||
expect(checkbox).toHaveAttribute("aria-disabled", "true");
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "-1");
|
||||
});
|
||||
|
||||
test("should have proper ARIA attributes when checked", () => {
|
||||
render(<Checkbox label="Test checkbox" checked={true} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).toHaveAttribute("role", "checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
test("should have proper ARIA attributes when unchecked", () => {
|
||||
render(<Checkbox label="Test checkbox" checked={false} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).toHaveAttribute("role", "checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
test("should have proper ARIA attributes with custom aria-label", () => {
|
||||
render(<Checkbox ariaLabel="Custom accessibility label" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Custom accessibility label",
|
||||
);
|
||||
});
|
||||
|
||||
test("should have proper focus management", () => {
|
||||
const { rerender } = render(<Checkbox label="Test checkbox" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
// Should be focusable when not disabled
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "0");
|
||||
|
||||
// Should not be focusable when disabled
|
||||
rerender(<Checkbox label="Test checkbox disabled" disabled={true} />);
|
||||
const disabledCheckbox = screen.getByRole("checkbox");
|
||||
expect(disabledCheckbox).toHaveAttribute("tabIndex", "-1");
|
||||
});
|
||||
|
||||
test("should have proper keyboard navigation", () => {
|
||||
render(<Checkbox label="Test checkbox" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
// Should be focusable
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "0");
|
||||
|
||||
// Should support keyboard interaction
|
||||
expect(checkbox).toHaveAttribute("role", "checkbox");
|
||||
});
|
||||
|
||||
test("should have proper semantic structure", () => {
|
||||
render(<Checkbox label="Test checkbox" />);
|
||||
|
||||
// Should have a label element
|
||||
const label = screen.getByText("Test checkbox").closest("label");
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
// Should have a checkbox role
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
|
||||
// Should be associated with the label
|
||||
expect(label).toContainElement(checkbox);
|
||||
});
|
||||
|
||||
test("should have proper color contrast", async () => {
|
||||
const { container } = render(<Checkbox label="Test checkbox" />);
|
||||
const results = await axe(container);
|
||||
|
||||
// Check for color contrast violations
|
||||
const contrastViolations = results.violations.filter(
|
||||
(violation) => violation.id === "color-contrast",
|
||||
);
|
||||
expect(contrastViolations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("should have proper focus indicators", async () => {
|
||||
const { container } = render(<Checkbox label="Test checkbox" />);
|
||||
const results = await axe(container);
|
||||
|
||||
// Check for focus indicator violations
|
||||
const focusViolations = results.violations.filter(
|
||||
(violation) => violation.id === "focus-order-semantics",
|
||||
);
|
||||
expect(focusViolations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("should have proper form integration", () => {
|
||||
render(<Checkbox name="test-checkbox" value="test-value" checked={true} />);
|
||||
|
||||
// Should have hidden input for form submission
|
||||
const hiddenInput = screen.getByDisplayValue("test-value");
|
||||
expect(hiddenInput).toBeInTheDocument();
|
||||
expect(hiddenInput).toHaveAttribute("type", "checkbox");
|
||||
expect(hiddenInput).toHaveAttribute("name", "test-checkbox");
|
||||
expect(hiddenInput).toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -1,234 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import RadioButton from "../../../app/components/RadioButton";
|
||||
|
||||
describe("RadioButton Accessibility", () => {
|
||||
it("has proper ARIA attributes", () => {
|
||||
render(<RadioButton label="Test Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("role", "radio");
|
||||
expect(radioButton).toHaveAttribute("aria-checked", "false");
|
||||
expect(radioButton).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
it("updates aria-checked when checked state changes", () => {
|
||||
const { rerender } = render(
|
||||
<RadioButton checked={false} label="Test Radio" />,
|
||||
);
|
||||
|
||||
let radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
rerender(<RadioButton checked={true} label="Test Radio" />);
|
||||
|
||||
radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("associates label with radio button", () => {
|
||||
render(<RadioButton label="Accessible Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
const labelId = radioButton.getAttribute("aria-labelledby");
|
||||
expect(labelId).toBeTruthy();
|
||||
|
||||
const labelElement = document.getElementById(labelId);
|
||||
expect(labelElement).toHaveTextContent("Accessible Radio");
|
||||
});
|
||||
|
||||
it("uses aria-label when provided", () => {
|
||||
render(<RadioButton ariaLabel="Custom Aria Label" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
|
||||
expect(radioButton).not.toHaveAttribute("aria-labelledby");
|
||||
});
|
||||
|
||||
it("prioritizes aria-label over aria-labelledby", () => {
|
||||
render(<RadioButton label="Visible Label" ariaLabel="Hidden Aria Label" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-label", "Hidden Aria Label");
|
||||
expect(radioButton).not.toHaveAttribute("aria-labelledby");
|
||||
});
|
||||
|
||||
it("is keyboard accessible", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(<RadioButton onChange={handleChange} label="Keyboard Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
radioButton.focus();
|
||||
|
||||
expect(radioButton).toHaveFocus();
|
||||
|
||||
await user.keyboard(" ");
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles Enter key activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(<RadioButton onChange={handleChange} label="Enter Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
await user.click(radioButton); // Focus the element first
|
||||
await user.keyboard("Enter");
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles Space key activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(<RadioButton onChange={handleChange} label="Space Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
radioButton.focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores other keys", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(<RadioButton onChange={handleChange} label="Other Keys Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
radioButton.focus();
|
||||
await user.keyboard("a");
|
||||
await user.keyboard("Tab");
|
||||
await user.keyboard("Escape");
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("has proper tab order", () => {
|
||||
render(
|
||||
<div>
|
||||
<RadioButton label="First Radio" />
|
||||
<RadioButton label="Second Radio" />
|
||||
<RadioButton label="Third Radio" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
|
||||
it("generates unique IDs for accessibility", () => {
|
||||
render(
|
||||
<div>
|
||||
<RadioButton label="Radio 1" />
|
||||
<RadioButton label="Radio 2" />
|
||||
<RadioButton label="Radio 3" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
const ids = radioButtons.map((button) => button.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
|
||||
expect(uniqueIds.size).toBe(3);
|
||||
expect(ids.every((id) => id.startsWith("radio-"))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses provided ID for accessibility", () => {
|
||||
render(<RadioButton id="custom-radio-id" label="Custom ID Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("id", "custom-radio-id");
|
||||
});
|
||||
|
||||
it("has accessible name from label", () => {
|
||||
render(<RadioButton label="Accessible Name Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
const accessibleName = radioButton.getAttribute("aria-labelledby");
|
||||
const labelElement = document.getElementById(accessibleName);
|
||||
|
||||
expect(labelElement).toHaveTextContent("Accessible Name Radio");
|
||||
});
|
||||
|
||||
it("has accessible name from aria-label", () => {
|
||||
render(<RadioButton ariaLabel="Aria Label Name" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-label", "Aria Label Name");
|
||||
});
|
||||
|
||||
it("maintains focus management", async () => {
|
||||
const handleChange = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<RadioButton
|
||||
checked={false}
|
||||
onChange={handleChange}
|
||||
label="Focus Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
radioButton.focus();
|
||||
expect(radioButton).toHaveFocus();
|
||||
|
||||
// Change checked state
|
||||
rerender(
|
||||
<RadioButton
|
||||
checked={true}
|
||||
onChange={handleChange}
|
||||
label="Focus Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should still be focusable
|
||||
expect(radioButton).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
it("has proper role and state", () => {
|
||||
render(<RadioButton checked={true} label="State Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("role", "radio");
|
||||
expect(radioButton).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("supports screen reader navigation", () => {
|
||||
render(
|
||||
<div>
|
||||
<RadioButton label="First Option" />
|
||||
<RadioButton label="Second Option" />
|
||||
<RadioButton label="Third Option" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
|
||||
// All should be in tab order
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveAttribute("tabIndex", "0");
|
||||
expect(button).toHaveAttribute("role", "radio");
|
||||
});
|
||||
});
|
||||
|
||||
it("has proper form association", () => {
|
||||
render(
|
||||
<RadioButton name="test-radio" value="test-value" label="Form Radio" />,
|
||||
);
|
||||
|
||||
const hiddenInput = screen.getByDisplayValue("test-value");
|
||||
expect(hiddenInput).toHaveAttribute("type", "radio");
|
||||
expect(hiddenInput).toHaveAttribute("name", "test-radio");
|
||||
expect(hiddenInput).toHaveAttribute("value", "test-value");
|
||||
});
|
||||
});
|
||||
@@ -1,316 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import RadioGroup from "../../../app/components/RadioGroup";
|
||||
|
||||
describe("RadioGroup Accessibility", () => {
|
||||
const defaultOptions = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
];
|
||||
|
||||
it("has proper radiogroup role", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes on radiogroup", () => {
|
||||
render(
|
||||
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
|
||||
);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
|
||||
});
|
||||
|
||||
it("has proper radio button roles", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons).toHaveLength(3);
|
||||
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveAttribute("role", "radio");
|
||||
expect(button).toHaveAttribute("aria-checked");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows correct selection state", () => {
|
||||
render(<RadioGroup options={defaultOptions} value="option2" />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
||||
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
|
||||
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
it("updates selection state correctly", () => {
|
||||
const { rerender } = render(
|
||||
<RadioGroup options={defaultOptions} value="option1" />,
|
||||
);
|
||||
|
||||
let radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
rerender(<RadioGroup options={defaultOptions} value="option3" />);
|
||||
|
||||
radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
||||
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("associates labels with radio buttons", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button, index) => {
|
||||
const labelId = button.getAttribute("aria-labelledby");
|
||||
expect(labelId).toBeTruthy();
|
||||
|
||||
const labelElement = document.getElementById(labelId);
|
||||
expect(labelElement).toHaveTextContent(`Option ${index + 1}`);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses aria-label when provided in options", () => {
|
||||
const optionsWithAria = [
|
||||
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
|
||||
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
|
||||
];
|
||||
|
||||
render(<RadioGroup options={optionsWithAria} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
|
||||
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
|
||||
});
|
||||
|
||||
it("is keyboard accessible", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
|
||||
// Focus first radio button
|
||||
radioButtons[0].focus();
|
||||
expect(radioButtons[0]).toHaveFocus();
|
||||
|
||||
// Navigate to second option
|
||||
radioButtons[1].focus();
|
||||
expect(radioButtons[1]).toHaveFocus();
|
||||
|
||||
// Activate with Space
|
||||
await user.keyboard(" ");
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
|
||||
});
|
||||
|
||||
it("handles Enter key activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
await user.click(radioButtons[2]); // Focus the element first
|
||||
await user.keyboard("Enter");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
|
||||
});
|
||||
|
||||
it("handles Space key activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons[1].focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
|
||||
});
|
||||
|
||||
it("ignores other keys", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons[1].focus();
|
||||
|
||||
await user.keyboard("a");
|
||||
await user.keyboard("Tab");
|
||||
await user.keyboard("Escape");
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("has proper tab order", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
|
||||
it("generates unique IDs for accessibility", () => {
|
||||
render(
|
||||
<div>
|
||||
<RadioGroup options={defaultOptions} />
|
||||
<RadioGroup options={defaultOptions} />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
const ids = radioButtons.map((button) => button.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
|
||||
// Should have unique IDs
|
||||
expect(uniqueIds.size).toBe(6);
|
||||
});
|
||||
|
||||
it("uses provided name for form association", () => {
|
||||
render(<RadioGroup options={defaultOptions} name="test-group" />);
|
||||
|
||||
const hiddenInputs = screen.getAllByDisplayValue("option1");
|
||||
hiddenInputs.forEach((input) => {
|
||||
expect(input).toHaveAttribute("name", "test-group");
|
||||
});
|
||||
});
|
||||
|
||||
it("has proper form association", () => {
|
||||
render(
|
||||
<RadioGroup options={defaultOptions} name="test-group" value="option2" />,
|
||||
);
|
||||
|
||||
const hiddenInputs = screen.getAllByDisplayValue("option1");
|
||||
expect(hiddenInputs[0]).toHaveAttribute("name", "test-group");
|
||||
expect(hiddenInputs[0]).toHaveAttribute("value", "option1");
|
||||
expect(hiddenInputs[0]).not.toBeChecked();
|
||||
|
||||
const option2Inputs = screen.getAllByDisplayValue("option2");
|
||||
expect(option2Inputs[0]).toHaveAttribute("name", "test-group");
|
||||
expect(option2Inputs[0]).toHaveAttribute("value", "option2");
|
||||
expect(option2Inputs[0]).toBeChecked();
|
||||
});
|
||||
|
||||
it("maintains focus management", async () => {
|
||||
const handleChange = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons[1].focus();
|
||||
expect(radioButtons[1]).toHaveFocus();
|
||||
|
||||
// Change selection
|
||||
rerender(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option2"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should still be focusable
|
||||
expect(radioButtons[1]).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
it("supports screen reader navigation", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
|
||||
// RadioGroup should be present
|
||||
expect(radioGroup).toBeInTheDocument();
|
||||
|
||||
// All radio buttons should be in tab order
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveAttribute("tabIndex", "0");
|
||||
expect(button).toHaveAttribute("role", "radio");
|
||||
});
|
||||
});
|
||||
|
||||
it("handles empty options gracefully", () => {
|
||||
render(<RadioGroup options={[]} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toBeInTheDocument();
|
||||
|
||||
const radioButtons = screen.queryAllByRole("radio");
|
||||
expect(radioButtons).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("has proper accessible names", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button, index) => {
|
||||
const labelId = button.getAttribute("aria-labelledby");
|
||||
const labelElement = document.getElementById(labelId);
|
||||
expect(labelElement).toHaveTextContent(`Option ${index + 1}`);
|
||||
});
|
||||
});
|
||||
|
||||
it("maintains single selection behavior", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
|
||||
// Click option 2 directly
|
||||
await user.click(radioButtons[1]);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
|
||||
|
||||
// Only one should be selected at a time
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { describe, test, expect, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Header from "../../../app/components/Header.js";
|
||||
import Footer from "../../../app/components/Footer.js";
|
||||
|
||||
// Extend expect to include accessibility matchers
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Accessibility - Component Level", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
// Set up proper language attribute for accessibility testing
|
||||
document.documentElement.setAttribute("lang", "en");
|
||||
});
|
||||
|
||||
test("Header component has no accessibility violations", async () => {
|
||||
const { container } = render(<Header />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Footer component has no accessibility violations", async () => {
|
||||
const { container } = render(<Footer />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Header has proper semantic structure", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check for banner landmark
|
||||
const banner = screen.getByRole("banner");
|
||||
expect(banner).toBeInTheDocument();
|
||||
|
||||
// Check for navigation landmark
|
||||
const navigation = screen.getByRole("navigation");
|
||||
expect(navigation).toBeInTheDocument();
|
||||
|
||||
// Check for proper heading structure (optional for header components)
|
||||
try {
|
||||
screen.getAllByRole("heading");
|
||||
// Headings are not required in header components, so this is optional
|
||||
} catch {
|
||||
// No headings found, which is fine for a header component
|
||||
}
|
||||
});
|
||||
|
||||
test("Header navigation items are accessible", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check that navigation items have proper roles
|
||||
const navigationItems = screen.getAllByRole("menuitem");
|
||||
expect(navigationItems.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that each navigation item has accessible text or aria-label
|
||||
navigationItems.forEach((item) => {
|
||||
const hasAccessibleText =
|
||||
item.textContent?.trim() || item.getAttribute("aria-label");
|
||||
expect(hasAccessibleText).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Header buttons have accessible names", () => {
|
||||
render(<Header />);
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
buttons.forEach((button) => {
|
||||
// Check for aria-label, aria-labelledby, or text content
|
||||
const hasAccessibleName =
|
||||
button.getAttribute("aria-label") ||
|
||||
button.getAttribute("aria-labelledby") ||
|
||||
button.textContent?.trim();
|
||||
|
||||
expect(hasAccessibleName).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Header images have alt text", () => {
|
||||
render(<Header />);
|
||||
|
||||
const images = screen.getAllByRole("img");
|
||||
images.forEach((image) => {
|
||||
const altText = image.getAttribute("alt");
|
||||
// Alt text should exist (can be empty for decorative images)
|
||||
expect(altText).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("Footer has proper semantic structure", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// Check for contentinfo landmark
|
||||
const contentinfo = screen.getByRole("contentinfo");
|
||||
expect(contentinfo).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Footer links are accessible", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
links.forEach((link) => {
|
||||
// Check for accessible text or aria-label
|
||||
const hasAccessibleText =
|
||||
link.textContent?.trim() || link.getAttribute("aria-label");
|
||||
|
||||
expect(hasAccessibleText).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Focus management works correctly", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Test that focusable elements can receive focus
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const links = screen.getAllByRole("link");
|
||||
|
||||
[...buttons, ...links].forEach((element) => {
|
||||
try {
|
||||
element.focus();
|
||||
expect(element).toHaveFocus();
|
||||
} catch {
|
||||
// Some elements might not be focusable in test environment
|
||||
// This is acceptable for accessibility testing
|
||||
// Intentionally ignore focus failures in JSDOM
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("Color contrast meets WCAG standards", async () => {
|
||||
const { container } = render(<Header />);
|
||||
const results = await axe(container, {
|
||||
rules: {
|
||||
"color-contrast": { enabled: true },
|
||||
},
|
||||
});
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Heading hierarchy is logical", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Try to get headings, but don't fail if none exist
|
||||
let headings;
|
||||
try {
|
||||
headings = screen.getAllByRole("heading");
|
||||
} catch {
|
||||
// No headings found, which is fine for a header component
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are no headings, that's fine for a header component
|
||||
if (headings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headingLevels = headings.map((heading) =>
|
||||
parseInt(heading.tagName.charAt(1)),
|
||||
);
|
||||
|
||||
// Check that heading levels are sequential (no skipping levels)
|
||||
for (let i = 1; i < headingLevels.length; i++) {
|
||||
const currentLevel = headingLevels[i];
|
||||
const previousLevel = headingLevels[i - 1];
|
||||
|
||||
// Heading levels should not skip more than one level
|
||||
expect(currentLevel - previousLevel).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("Interactive elements have proper ARIA attributes", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Get all interactive elements
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const links = screen.getAllByRole("link");
|
||||
const menuitems = screen.getAllByRole("menuitem");
|
||||
|
||||
const interactiveElements = [...buttons, ...links, ...menuitems];
|
||||
|
||||
interactiveElements.forEach((element) => {
|
||||
// Check for proper ARIA attributes
|
||||
const role = element.getAttribute("role");
|
||||
if (role) {
|
||||
// If role is specified, it should be valid
|
||||
const validRoles = [
|
||||
"button",
|
||||
"link",
|
||||
"menuitem",
|
||||
"navigation",
|
||||
"banner",
|
||||
];
|
||||
expect(validRoles).toContain(role);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("No duplicate IDs exist", async () => {
|
||||
const { container } = render(<Header />);
|
||||
const results = await axe(container, {
|
||||
rules: {
|
||||
"duplicate-id": { enabled: true },
|
||||
},
|
||||
});
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("Proper language attributes", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check that the document has proper language attributes
|
||||
const html = document.documentElement;
|
||||
const lang = html.getAttribute("lang");
|
||||
expect(lang).toBeTruthy();
|
||||
expect(lang).toMatch(/^[a-z]{2}(-[A-Z]{2})?$/);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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)]");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user