Select and Context Menu component with storybook and testing
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, test, 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();
|
||||
});
|
||||
});
|
||||
@@ -98,7 +98,7 @@ describe("Input Component", () => {
|
||||
test("applies correct size classes", () => {
|
||||
const { rerender } = render(<Input size="small" />);
|
||||
let input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("h-[30px]");
|
||||
expect(input).toHaveClass("h-[32px]");
|
||||
|
||||
rerender(<Input size="medium" />);
|
||||
input = screen.getByRole("textbox");
|
||||
@@ -146,9 +146,9 @@ describe("Input Component", () => {
|
||||
test("applies hover state classes", () => {
|
||||
render(<Input state="hover" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("border-2");
|
||||
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
|
||||
expect(input).toHaveClass(
|
||||
"border-[var(--color-border-default-brand-primary)]"
|
||||
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -162,7 +162,9 @@ describe("Input Component", () => {
|
||||
render(<Input state="default" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
|
||||
expect(input).toHaveClass("hover:outline");
|
||||
expect(input).toHaveClass(
|
||||
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]"
|
||||
);
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, test, 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
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user