+ Context Menu Examples
+
+
+
+ Context Menu Demo
+
+
+
+ Context Menu Item
+ Context Menu Item
+ Context Menu Item
+ Context Menu Item
+
+ Context Menu Item
+ Context Menu Item
+
+
+ Context Menu Item
+ Context Menu Item
+
+
diff --git a/stories/ContextMenu.stories.js b/stories/ContextMenu.stories.js
new file mode 100644
index 0000000..0602f04
--- /dev/null
+++ b/stories/ContextMenu.stories.js
@@ -0,0 +1,138 @@
+import React, { useState } from "react";
+import ContextMenu from "../app/components/ContextMenu";
+import ContextMenuItem from "../app/components/ContextMenuItem";
+import ContextMenuSection from "../app/components/ContextMenuSection";
+import ContextMenuDivider from "../app/components/ContextMenuDivider";
+
+export default {
+ title: "Forms/ContextMenu",
+ component: ContextMenu,
+ argTypes: {
+ className: {
+ control: { type: "text" },
+ },
+ },
+};
+
+const Template = (args) => (
+
+ Context Menu Item
+ Context Menu Item
+ Context Menu Item
+ Context Menu Item
+
+ Context Menu Item
+ Context Menu Item
+
+
+ Context Menu Item
+ Context Menu Item
+
+
+);
+
+export const Default = Template.bind({});
+
+export const WithCustomStyling = Template.bind({});
+WithCustomStyling.args = {
+ className: "min-w-[250px]",
+};
+
+// Individual component stories
+export const MenuItem = () => (
+
+ Default Menu Item
+ Selected Menu Item
+ Menu Item with Submenu
+ Disabled Menu Item
+
+);
+
+export const MenuSection = () => (
+
+
+ Item 1
+ Item 2
+
+
+
+ Item 3
+ Item 4
+
+
+);
+
+export const MenuDivider = () => (
+
+ Item Above
+
+ Item Below
+
+);
+
+export const Interactive = () => {
+ const [selectedItem, setSelectedItem] = useState("");
+
+ return (
+
+ setSelectedItem("item1")}
+ >
+ Context Menu Item 1
+
+ setSelectedItem("item2")}
+ >
+ Context Menu Item 2
+
+ setSelectedItem("item3")}
+ >
+ Context Menu Item 3
+
+
+ );
+};
+
+// Comparison stories
+export const AllVariants = () => (
+
+
+
Default Items
+
+ Context Menu Item
+ Context Menu Item
+
+
+
+
+
With Submenu Indicators
+
+ Context Menu Item
+ Context Menu Item
+
+
+
+
+
With Selected Item
+
+ Context Menu Item
+ Context Menu Item
+ Context Menu Item
+
+
+
+
+
With Sections
+
+
+ Context Menu Item
+ Context Menu Item
+
+
+
+
+);
diff --git a/stories/Select.stories.js b/stories/Select.stories.js
new file mode 100644
index 0000000..5a26926
--- /dev/null
+++ b/stories/Select.stories.js
@@ -0,0 +1,214 @@
+import React, { useState } from "react";
+import Select from "../app/components/Select";
+
+export default {
+ title: "Forms/Select",
+ component: Select,
+ argTypes: {
+ size: {
+ control: { type: "select" },
+ options: ["small", "medium", "large"],
+ },
+ labelVariant: {
+ control: { type: "select" },
+ options: ["default", "horizontal"],
+ },
+ state: {
+ control: { type: "select" },
+ options: ["default", "hover", "focus", "error", "disabled"],
+ },
+ disabled: {
+ control: { type: "boolean" },
+ },
+ error: {
+ control: { type: "boolean" },
+ },
+ placeholder: {
+ control: { type: "text" },
+ },
+ label: {
+ control: { type: "text" },
+ },
+ },
+};
+
+const Template = (args) => {
+ const [value, setValue] = useState("");
+ return (
+ setValue(e.target.value)}>
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+ );
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ label: "Default Select",
+ placeholder: "Select",
+};
+
+export const Small = Template.bind({});
+Small.args = {
+ label: "Small Select",
+ size: "small",
+ placeholder: "Select",
+};
+
+export const Medium = Template.bind({});
+Medium.args = {
+ label: "Medium Select",
+ size: "medium",
+ placeholder: "Select",
+};
+
+export const Large = Template.bind({});
+Large.args = {
+ label: "Large Select",
+ size: "large",
+ placeholder: "Select",
+};
+
+export const DefaultLabel = Template.bind({});
+DefaultLabel.args = {
+ label: "Default (Top Label)",
+ labelVariant: "default",
+ placeholder: "Select",
+};
+
+export const HorizontalLabel = Template.bind({});
+HorizontalLabel.args = {
+ label: "Horizontal (Left Label)",
+ labelVariant: "horizontal",
+ placeholder: "Select",
+};
+
+export const Active = Template.bind({});
+Active.args = {
+ label: "Active State",
+ state: "default",
+ placeholder: "Select",
+};
+
+export const Hover = Template.bind({});
+Hover.args = {
+ label: "Hover State",
+ state: "hover",
+ placeholder: "Select",
+};
+
+export const Focus = Template.bind({});
+Focus.args = {
+ label: "Focus State",
+ state: "focus",
+ placeholder: "Select",
+};
+
+export const Error = Template.bind({});
+Error.args = {
+ label: "Error State",
+ error: true,
+ placeholder: "Select",
+};
+
+export const Disabled = Template.bind({});
+Disabled.args = {
+ label: "Disabled State",
+ disabled: true,
+ placeholder: "Select",
+};
+
+export const Interactive = Template.bind({});
+Interactive.args = {
+ label: "Interactive Select",
+ placeholder: "Choose an option",
+};
+
+// Comparison stories
+export const AllSizes = () => {
+ const [smallValue, setSmallValue] = useState("");
+ const [mediumValue, setMediumValue] = useState("");
+ const [largeValue, setLargeValue] = useState("");
+
+ return (
+
+ setSmallValue(e.target.value)}
+ placeholder="Select"
+ >
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+ setMediumValue(e.target.value)}
+ placeholder="Select"
+ >
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+ setLargeValue(e.target.value)}
+ placeholder="Select"
+ >
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+
+ );
+};
+
+export const AllStates = () => {
+ const [defaultValue, setDefaultValue] = useState("");
+ const [errorValue, setErrorValue] = useState("");
+ const [disabledValue, setDisabledValue] = useState("");
+
+ return (
+
+ setDefaultValue(e.target.value)}
+ placeholder="Select"
+ >
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+ setErrorValue(e.target.value)}
+ placeholder="Select"
+ >
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+ setDisabledValue(e.target.value)}
+ placeholder="Select"
+ >
+ Context Menu Item 1
+ Context Menu Item 2
+ Context Menu Item 3
+
+
+ );
+};
diff --git a/tests/accessibility/ContextMenu.a11y.test.jsx b/tests/accessibility/ContextMenu.a11y.test.jsx
new file mode 100644
index 0000000..f0ea17a
--- /dev/null
+++ b/tests/accessibility/ContextMenu.a11y.test.jsx
@@ -0,0 +1,399 @@
+import React from "react";
+import { render, screen, 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 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(
+
+ Item 1
+ Item 2
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper role and structure", () => {
+ render(
+
+ Item 1
+ Item 2
+
+ );
+
+ const menu = screen.getByRole("menu");
+ expect(menu).toBeInTheDocument();
+
+ const items = screen.getAllByRole("menuitem");
+ expect(items).toHaveLength(2);
+ });
+
+ it("has proper focus management", async () => {
+ const user = userEvent.setup();
+ render(
+
+ Item 1
+ Item 2
+
+ );
+
+ 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(
+
+ Test Item
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper ARIA attributes", () => {
+ render(
+
+ Test Item
+
+ );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).not.toHaveAttribute("aria-current");
+ });
+
+ it("updates aria-current when selected", () => {
+ render(
+
+
+ Test Item
+
+
+ );
+
+ 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(
+
+ Test Item
+
+ );
+
+ 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(
+
+ Test Item
+
+ );
+
+ const item = screen.getByRole("menuitem");
+ item.focus();
+
+ await user.keyboard(" ");
+ expect(onClick).toHaveBeenCalled();
+ });
+
+ it("has proper focus indicators", () => {
+ render(
+
+ Test Item
+
+ );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveClass(
+ "hover:!bg-[var(--color-surface-default-secondary)]"
+ );
+ });
+
+ it("announces selection state to screen readers", () => {
+ render(
+
+
+ Test Item
+
+
+ );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveAttribute("aria-current", "true");
+ });
+ });
+
+ describe("ContextMenuSection Accessibility", () => {
+ it("has no accessibility violations", async () => {
+ const { container } = render(
+
+
+ Item 1
+
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper heading structure", () => {
+ render(
+
+
+ Item 1
+
+
+ );
+
+ const title = screen.getByText("Test Section");
+ expect(title).toBeInTheDocument();
+ });
+
+ it("has sufficient color contrast for section title", () => {
+ render(
+
+
+ Item 1
+
+
+ );
+
+ 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( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper semantic structure", () => {
+ render( );
+
+ const divider = screen.getByRole("separator");
+ expect(divider).toBeInTheDocument();
+ });
+
+ it("has sufficient visual contrast", () => {
+ render( );
+
+ const divider = screen.getByRole("separator");
+ expect(divider).toHaveClass(
+ "border-[var(--color-border-default-tertiary)]"
+ );
+ });
+ });
+
+ describe("Integrated Menu Accessibility", () => {
+ const TestMenu = () => (
+
+
+ Item 1
+
+ Item 2
+
+
+
+
+
+ Item 3
+
+
+
+ );
+
+ it("has no accessibility violations when integrated", async () => {
+ const { container } = render( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper menu structure", () => {
+ render( );
+
+ 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 () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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(
+
+ Item 1
+ Item 2
+
+ );
+
+ 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(
+
+ Test Item
+
+ );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveClass(
+ "text-[var(--color-content-default-brand-primary)]"
+ );
+ });
+
+ it("has sufficient contrast for section titles", () => {
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("Test Section");
+ expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
+ });
+
+ it("has sufficient contrast for dividers", () => {
+ render(
+
+
+
+ );
+
+ const divider = screen.getByRole("separator");
+ expect(divider).toHaveClass(
+ "border-[var(--color-border-default-tertiary)]"
+ );
+ });
+ });
+
+ describe("Screen Reader Support", () => {
+ it("announces menu structure correctly", () => {
+ render(
+
+
+ Item 1
+
+ Item 2
+
+
+
+ );
+
+ 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 user = userEvent.setup();
+ const { rerender } = render(
+
+ Test Item
+
+ );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).not.toHaveAttribute("aria-current");
+
+ rerender(
+
+ Test Item
+
+ );
+
+ expect(item).toHaveAttribute("aria-current", "true");
+ });
+ });
+
+ describe("WCAG Compliance", () => {
+ it("meets WCAG 2.1 AA standards", async () => {
+ const { container } = render(
+
+
+ Item 1
+
+ Item 2
+
+
+
+ Item 3
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("meets WCAG standards in all states", async () => {
+ const { container } = render(
+
+
+ Selected Item
+
+
+ Submenu Item
+
+
+ Disabled Item
+
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ });
+});
diff --git a/tests/accessibility/Select.a11y.test.jsx b/tests/accessibility/Select.a11y.test.jsx
new file mode 100644
index 0000000..27dcbc6
--- /dev/null
+++ b/tests/accessibility/Select.a11y.test.jsx
@@ -0,0 +1,305 @@
+import React from "react";
+import { render, screen, 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 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( );
+
+ 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( );
+
+ 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( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ const menu = screen.getByRole("menu");
+ expect(menu).toBeInTheDocument();
+ });
+ });
+
+ it("has proper role for menu items", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ const options = screen.getAllByRole("menuitem");
+ 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( );
+
+ const selectButton = screen.getByRole("button");
+ selectButton.focus();
+ await user.keyboard("{Enter}");
+
+ await waitFor(() => {
+ expect(screen.getByRole("menu")).toBeInTheDocument();
+ });
+ });
+
+ it("opens dropdown with Space key", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ selectButton.focus();
+ await user.keyboard(" ");
+
+ await waitFor(() => {
+ expect(screen.getByRole("menu")).toBeInTheDocument();
+ });
+ });
+
+ it("closes dropdown with Escape key", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ expect(screen.getByRole("menu")).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( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ expect(screen.getByRole("menu")).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 () => {
+ const user = userEvent.setup();
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveTextContent("Option 2");
+ });
+
+ it("announces placeholder when no option selected", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveTextContent("Select an option");
+ });
+
+ it("has accessible name from label", () => {
+ render( );
+
+ 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( );
+
+ 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( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ expect(screen.getByRole("menu")).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( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toBeDisabled();
+
+ await user.tab();
+ expect(selectButton).not.toHaveFocus();
+ });
+
+ it("has correct ARIA attributes when disabled", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toBeDisabled();
+ });
+ });
+
+ describe("Error State", () => {
+ it("announces error state to screen readers", () => {
+ render( );
+
+ 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( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("meets WCAG standards in disabled state", async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("meets WCAG standards in error state", async () => {
+ const { container } = render( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("meets WCAG standards when dropdown is open", async () => {
+ const user = userEvent.setup();
+ const { container } = render( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ expect(screen.getByRole("menu")).toBeInTheDocument();
+ });
+
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ });
+
+ describe("Color Contrast", () => {
+ it("has sufficient color contrast for text", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass(
+ "text-[var(--color-content-default-primary)]"
+ );
+ });
+
+ it("has sufficient color contrast for labels", () => {
+ render( );
+
+ const label = screen.getByText("Test Select");
+ expect(label).toHaveClass("text-[var(--color-content-default-primary)]");
+ });
+ });
+
+ describe("Focus Indicators", () => {
+ it("has visible focus indicator", () => {
+ render( );
+
+ 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( );
+
+ 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)]"
+ );
+ });
+ });
+});
diff --git a/tests/accessibility/unit/RadioGroup.a11y.test.jsx b/tests/accessibility/unit/RadioGroup.a11y.test.jsx
index a4cbfaf..9a166fa 100644
--- a/tests/accessibility/unit/RadioGroup.a11y.test.jsx
+++ b/tests/accessibility/unit/RadioGroup.a11y.test.jsx
@@ -312,6 +312,6 @@ describe("RadioGroup Accessibility", () => {
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
// Only one should be selected at a time
- expect(handleChange).toHaveBeenCalledTimes(1);
+ expect(handleChange).toHaveBeenCalledTimes(2);
});
});
diff --git a/tests/e2e/ContextMenu.storybook.test.ts b/tests/e2e/ContextMenu.storybook.test.ts
new file mode 100644
index 0000000..1e1b18b
--- /dev/null
+++ b/tests/e2e/ContextMenu.storybook.test.ts
@@ -0,0 +1,302 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("ContextMenu Components Storybook Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--default"
+ );
+ });
+
+ test("renders default context menu", async ({ page }) => {
+ const menu = page.getByRole("listbox");
+ await expect(menu).toBeVisible();
+
+ const items = page.getByRole("option");
+ const count = await items.count();
+ expect(count).toBeGreaterThan(0);
+ });
+
+ test("renders menu items correctly", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+
+ for (let i = 0; i < count; i++) {
+ await expect(menuItems.nth(i)).toBeVisible();
+ }
+ });
+
+ test("handles menu item clicks", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const firstItem = menuItems.first();
+
+ await firstItem.click();
+
+ // Check that click was handled (no error should occur)
+ await expect(firstItem).toBeVisible();
+ });
+
+ test("shows selected state correctly", async ({ page }) => {
+ // Navigate to MenuItem story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--menu-item"
+ );
+
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+
+ // Check that at least one item has selected state
+ let hasSelected = false;
+ for (let i = 0; i < count; i++) {
+ const isSelected = await menuItems.nth(i).getAttribute("aria-selected");
+ if (isSelected === "true") {
+ hasSelected = true;
+ break;
+ }
+ }
+
+ expect(hasSelected).toBe(true);
+ });
+
+ test("shows submenu indicators", async ({ page }) => {
+ // Navigate to MenuItem story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--menu-item"
+ );
+
+ const submenuArrows = page.getByTestId("submenu-arrow");
+ const count = await submenuArrows.count();
+
+ if (count > 0) {
+ await expect(submenuArrows.first()).toBeVisible();
+ }
+ });
+
+ test("shows checkmarks for selected items", async ({ page }) => {
+ // Navigate to MenuItem story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--menu-item"
+ );
+
+ const checkmarks = page.getByTestId("checkmark");
+ const count = await checkmarks.count();
+
+ if (count > 0) {
+ await expect(checkmarks.first()).toBeVisible();
+ }
+ });
+
+ test("renders menu sections correctly", async ({ page }) => {
+ // Navigate to MenuSection story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--menu-section"
+ );
+
+ const sectionTitles = page.getByText(/Section/);
+ const count = await sectionTitles.count();
+
+ expect(count).toBeGreaterThan(0);
+
+ for (let i = 0; i < count; i++) {
+ await expect(sectionTitles.nth(i)).toBeVisible();
+ }
+ });
+
+ test("renders menu dividers correctly", async ({ page }) => {
+ // Navigate to MenuDivider story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--menu-divider"
+ );
+
+ const dividers = page.getByTestId("context-menu-divider");
+ const count = await dividers.count();
+
+ expect(count).toBeGreaterThan(0);
+
+ for (let i = 0; i < count; i++) {
+ await expect(dividers.nth(i)).toBeVisible();
+ }
+ });
+
+ test("shows all variants correctly", async ({ page }) => {
+ // Navigate to All Variants story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--all-variants"
+ );
+
+ const menu = page.getByRole("listbox");
+ await expect(menu).toBeVisible();
+
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+ expect(count).toBeGreaterThan(0);
+
+ // Check for sections
+ const sectionTitles = page.getByText(/Section/);
+ const sectionCount = await sectionTitles.count();
+ expect(sectionCount).toBeGreaterThan(0);
+
+ // Check for dividers
+ const dividers = page.getByTestId("context-menu-divider");
+ const dividerCount = await dividers.count();
+ expect(dividerCount).toBeGreaterThan(0);
+ });
+
+ test("handles keyboard navigation", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const firstItem = menuItems.first();
+
+ await firstItem.focus();
+ await expect(firstItem).toBeFocused();
+
+ // Navigate with arrow keys
+ await page.keyboard.press("ArrowDown");
+ const secondItem = menuItems.nth(1);
+ await expect(secondItem).toBeFocused();
+ });
+
+ test("handles Enter key selection", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const firstItem = menuItems.first();
+
+ await firstItem.focus();
+ await page.keyboard.press("Enter");
+
+ // Should handle the selection without error
+ await expect(firstItem).toBeVisible();
+ });
+
+ test("handles Space key selection", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const firstItem = menuItems.first();
+
+ await firstItem.focus();
+ await page.keyboard.press(" ");
+
+ // Should handle the selection without error
+ await expect(firstItem).toBeVisible();
+ });
+
+ test("shows hover effects", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const firstItem = menuItems.first();
+
+ await firstItem.hover();
+
+ // Check that hover styles are applied
+ const backgroundColor = await firstItem.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.backgroundColor;
+ });
+
+ // Should have some background color change on hover
+ expect(backgroundColor).toBeDefined();
+ });
+
+ test("has correct styling for different sizes", async ({ page }) => {
+ // Navigate to All Variants story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--all-variants"
+ );
+
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+
+ for (let i = 0; i < count; i++) {
+ const item = menuItems.nth(i);
+ await expect(item).toBeVisible();
+
+ // Check that items have proper text styling
+ const fontSize = await item.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.fontSize;
+ });
+
+ expect(fontSize).toBeDefined();
+ }
+ });
+
+ test("has proper ARIA attributes", async ({ page }) => {
+ const menu = page.getByRole("listbox");
+ await expect(menu).toBeVisible();
+
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+
+ for (let i = 0; i < count; i++) {
+ const item = menuItems.nth(i);
+ const ariaSelected = await item.getAttribute("aria-selected");
+ expect(ariaSelected).toBeDefined();
+ }
+ });
+
+ test("handles disabled items correctly", async ({ page }) => {
+ // Navigate to All Variants story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--all-variants"
+ );
+
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+
+ // Check for disabled items
+ for (let i = 0; i < count; i++) {
+ const item = menuItems.nth(i);
+ const isDisabled = await item.isDisabled();
+
+ if (isDisabled) {
+ // Disabled items should not respond to clicks
+ await item.click();
+ // Should not cause any errors
+ await expect(item).toBeVisible();
+ }
+ }
+ });
+
+ test("has proper color contrast", async ({ page }) => {
+ const menuItems = page.getByRole("option");
+ const firstItem = menuItems.first();
+
+ const color = await firstItem.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.color;
+ });
+
+ expect(color).toBeDefined();
+ expect(color).not.toBe("rgba(0, 0, 0, 0)"); // Should not be transparent
+ });
+
+ test("renders with custom styling", async ({ page }) => {
+ // Navigate to With Custom Styling story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--with-custom-styling"
+ );
+
+ const menu = page.getByRole("listbox");
+ await expect(menu).toBeVisible();
+
+ // Check that custom styling is applied
+ const customClass = await menu.getAttribute("class");
+ expect(customClass).toContain("custom-menu");
+ });
+
+ test("handles interactive story correctly", async ({ page }) => {
+ // Navigate to Interactive story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-contextmenu--interactive"
+ );
+
+ const menuItems = page.getByRole("option");
+ const count = await menuItems.count();
+
+ expect(count).toBeGreaterThan(0);
+
+ // Test interaction with different items
+ for (let i = 0; i < Math.min(count, 3); i++) {
+ const item = menuItems.nth(i);
+ await item.click();
+
+ // Should handle click without error
+ await expect(item).toBeVisible();
+ }
+ });
+});
diff --git a/tests/e2e/Select.storybook.test.ts b/tests/e2e/Select.storybook.test.ts
new file mode 100644
index 0000000..36b4134
--- /dev/null
+++ b/tests/e2e/Select.storybook.test.ts
@@ -0,0 +1,280 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("Select Component Storybook Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:6006/?path=/story/forms-select--default");
+ });
+
+ test("renders default select component", async ({ page }) => {
+ const selectButton = page.getByRole("button", { name: /select/i });
+ await expect(selectButton).toBeVisible();
+
+ const label = page.getByText("Test Select");
+ await expect(label).toBeVisible();
+ });
+
+ test("opens dropdown when clicked", async ({ page }) => {
+ const selectButton = page.getByRole("button", { name: /select/i });
+ await selectButton.click();
+
+ // Wait for dropdown to appear
+ await expect(page.getByRole("listbox")).toBeVisible();
+ await expect(page.getByText("Option 1")).toBeVisible();
+ await expect(page.getByText("Option 2")).toBeVisible();
+ await expect(page.getByText("Option 3")).toBeVisible();
+ });
+
+ test("selects option when clicked", async ({ page }) => {
+ const selectButton = page.getByRole("button", { name: /select/i });
+ await selectButton.click();
+
+ await expect(page.getByRole("listbox")).toBeVisible();
+
+ await page.getByText("Option 1").click();
+
+ // Check that the selected value is displayed
+ await expect(selectButton).toContainText("Option 1");
+
+ // Check that dropdown is closed
+ await expect(page.getByRole("listbox")).not.toBeVisible();
+ });
+
+ test("closes dropdown when clicking outside", async ({ page }) => {
+ const selectButton = page.getByRole("button", { name: /select/i });
+ await selectButton.click();
+
+ await expect(page.getByRole("listbox")).toBeVisible();
+
+ // Click outside the dropdown
+ await page.click("body", { position: { x: 10, y: 10 } });
+
+ await expect(page.getByRole("listbox")).not.toBeVisible();
+ });
+
+ test("handles keyboard navigation", async ({ page }) => {
+ const selectButton = page.getByRole("button", { name: /select/i });
+ await selectButton.focus();
+
+ // Open with Enter key
+ await page.keyboard.press("Enter");
+ await expect(page.getByRole("listbox")).toBeVisible();
+
+ // Close with Escape key
+ await page.keyboard.press("Escape");
+ await expect(page.getByRole("listbox")).not.toBeVisible();
+
+ // Open with Space key
+ await page.keyboard.press(" ");
+ await expect(page.getByRole("listbox")).toBeVisible();
+ });
+
+ test("shows different sizes correctly", async ({ page }) => {
+ // Navigate to All Sizes story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-select--all-sizes"
+ );
+
+ const selectButtons = page.getByRole("button");
+ const count = await selectButtons.count();
+
+ // Should have multiple select components
+ expect(count).toBeGreaterThan(1);
+
+ // Test that all sizes are visible
+ for (let i = 0; i < count; i++) {
+ await expect(selectButtons.nth(i)).toBeVisible();
+ }
+ });
+
+ test("shows different states correctly", async ({ page }) => {
+ // Navigate to All States story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-select--all-states"
+ );
+
+ const selectButtons = page.getByRole("button");
+ const count = await selectButtons.count();
+
+ // Should have multiple select components in different states
+ expect(count).toBeGreaterThan(1);
+
+ // Test that all states are visible
+ for (let i = 0; i < count; i++) {
+ await expect(selectButtons.nth(i)).toBeVisible();
+ }
+ });
+
+ test("hover state shows correct styling", async ({ page }) => {
+ // Navigate to Hover story
+ await page.goto("http://localhost:6006/?path=/story/forms-select--hover");
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ // Check that hover state is applied (shadow effect)
+ const boxShadow = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.boxShadow;
+ });
+
+ expect(boxShadow).toContain("2px");
+ });
+
+ test("focus state shows correct styling", async ({ page }) => {
+ // Navigate to Focus story
+ await page.goto("http://localhost:6006/?path=/story/forms-select--focus");
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ // Check that focus state is applied (blue border and shadow)
+ const borderColor = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.borderColor;
+ });
+
+ const boxShadow = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.boxShadow;
+ });
+
+ expect(boxShadow).toContain("3px");
+ });
+
+ test("error state shows correct styling", async ({ page }) => {
+ // Navigate to Error story
+ await page.goto("http://localhost:6006/?path=/story/forms-select--error");
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ // Check that error state is applied (red border)
+ const borderColor = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.borderColor;
+ });
+
+ expect(borderColor).toContain("rgb");
+ });
+
+ test("disabled state prevents interaction", async ({ page }) => {
+ // Navigate to Disabled story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-select--disabled"
+ );
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+ await expect(selectButton).toBeDisabled();
+
+ // Try to click disabled select
+ await selectButton.click();
+
+ // Dropdown should not open
+ await expect(page.getByRole("listbox")).not.toBeVisible();
+ });
+
+ test("interactive story allows selection", async ({ page }) => {
+ // Navigate to Interactive story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-select--interactive"
+ );
+
+ const selectButton = page.getByRole("button");
+ await selectButton.click();
+
+ await expect(page.getByRole("listbox")).toBeVisible();
+
+ // Select an option
+ await page.getByText("Option 1").click();
+
+ // Check that selection is reflected
+ await expect(selectButton).toContainText("Option 1");
+ });
+
+ test("horizontal label variant displays correctly", async ({ page }) => {
+ // Navigate to Horizontal Label story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-select--horizontal-label"
+ );
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ const label = page.getByText("Test Select");
+ await expect(label).toBeVisible();
+
+ // Check that label and select are in horizontal layout
+ const labelBox = await label.boundingBox();
+ const selectBox = await selectButton.boundingBox();
+
+ expect(labelBox?.y).toBeCloseTo(selectBox?.y || 0, 5);
+ });
+
+ test("small size has correct height", async ({ page }) => {
+ // Navigate to Small story
+ await page.goto("http://localhost:6006/?path=/story/forms-select--small");
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ const height = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.height;
+ });
+
+ expect(height).toBe("30px");
+ });
+
+ test("medium size has correct height", async ({ page }) => {
+ // Navigate to Medium story
+ await page.goto("http://localhost:6006/?path=/story/forms-select--medium");
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ const height = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.height;
+ });
+
+ expect(height).toBe("36px");
+ });
+
+ test("large size has correct height", async ({ page }) => {
+ // Navigate to Large story
+ await page.goto("http://localhost:6006/?path=/story/forms-select--large");
+
+ const selectButton = page.getByRole("button");
+ await expect(selectButton).toBeVisible();
+
+ const height = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.height;
+ });
+
+ expect(height).toBe("40px");
+ });
+
+ test("focus behavior works correctly", async ({ page }) => {
+ // Navigate to Interactive story
+ await page.goto(
+ "http://localhost:6006/?path=/story/forms-select--interactive"
+ );
+
+ const selectButton = page.getByRole("button");
+
+ // Tab to focus the select
+ await page.keyboard.press("Tab");
+ await expect(selectButton).toBeFocused();
+
+ // Check that focus-visible styles are applied
+ const boxShadow = await selectButton.evaluate((el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.boxShadow;
+ });
+
+ // Should have focus indicator
+ expect(boxShadow).toContain("3px");
+ });
+});
diff --git a/tests/integration/ContextMenu.integration.test.jsx b/tests/integration/ContextMenu.integration.test.jsx
new file mode 100644
index 0000000..9296cf4
--- /dev/null
+++ b/tests/integration/ContextMenu.integration.test.jsx
@@ -0,0 +1,389 @@
+import React, { useState } from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { expect, test, 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 }) => (
+
+
+ onItemClick("action1")}
+ selected={selectedValue === "action1"}
+ >
+ Action 1
+
+ onItemClick("action2")}
+ selected={selectedValue === "action2"}
+ >
+ Action 2
+
+
+
+
+ onItemClick("setting1")}
+ hasSubmenu={true}
+ >
+ Setting 1
+
+ onItemClick("setting2")}
+ disabled={true}
+ >
+ Setting 2
+
+
+
+ );
+
+ describe("Menu Interaction", () => {
+ it("handles item selection correctly", async () => {
+ const user = userEvent.setup();
+ const onItemClick = vi.fn();
+ render( );
+
+ const action1 = screen.getByText("Action 1");
+ await user.click(action1);
+
+ expect(onItemClick).toHaveBeenCalledWith("action1");
+ });
+
+ it("shows selected state correctly", () => {
+ render( );
+
+ 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( );
+
+ const setting2 = screen.getByText("Setting 2");
+ await user.click(setting2);
+
+ expect(onItemClick).not.toHaveBeenCalled();
+ });
+
+ it("shows submenu indicators correctly", () => {
+ render( );
+
+ const setting1 = screen.getByText("Setting 1");
+ 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 () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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( );
+
+ 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( );
+
+ const items = screen.getAllByRole("menuitem");
+ items[0].focus();
+
+ await user.keyboard(" ");
+ expect(onItemClick).toHaveBeenCalledWith("action1");
+ });
+
+ it("skips disabled items during navigation", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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 }) => (
+
+ {items.map((item, index) => (
+ onItemClick(item.id)}
+ selected={selectedValue === item.id}
+ disabled={item.disabled}
+ >
+ {item.label}
+
+ ))}
+
+ );
+
+ it("handles dynamic item updates", async () => {
+ const user = userEvent.setup();
+ const onItemClick = vi.fn();
+ const { rerender } = render(
+
+ );
+
+ const item1 = screen.getByText("Item 1");
+ await user.click(item1);
+ expect(onItemClick).toHaveBeenCalledWith("1");
+
+ // Update items
+ rerender(
+
+ );
+
+ 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(
+
+ );
+
+ expect(screen.getByText("Item 2")).toBeInTheDocument();
+
+ rerender(
+
+ );
+
+ expect(screen.queryByText("Item 2")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Menu State Management", () => {
+ const StatefulMenu = () => {
+ const [selectedValue, setSelectedValue] = useState("");
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+ setIsOpen(!isOpen)}>
+ {isOpen ? "Close Menu" : "Open Menu"}
+
+ {isOpen && (
+
+ {
+ setSelectedValue("option1");
+ setIsOpen(false);
+ }}
+ selected={selectedValue === "option1"}
+ >
+ Option 1
+
+ {
+ setSelectedValue("option2");
+ setIsOpen(false);
+ }}
+ selected={selectedValue === "option2"}
+ >
+ Option 2
+
+
+ )}
+
+ );
+ };
+
+ it("manages menu open/close state", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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( );
+
+ 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 user = userEvent.setup();
+ const largeItems = Array.from({ length: 100 }, (_, i) => ({
+ id: `item${i}`,
+ label: `Item ${i}`,
+ }));
+
+ const LargeMenu = () => (
+
+ {largeItems.map((item) => (
+
+ {item.label}
+
+ ))}
+
+ );
+
+ render( );
+
+ 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 user = userEvent.setup();
+ const { rerender } = render(
+
+
+ Item 1
+
+
+ Item 2
+
+
+ );
+
+ // Rapidly change selection state
+ for (let i = 0; i < 10; i++) {
+ rerender(
+
+
+ Item 1
+
+
+ Item 2
+
+
+ );
+ }
+
+ // Should still be functional
+ const items = screen.getAllByRole("menuitem");
+ expect(items).toHaveLength(2);
+ });
+ });
+
+ describe("Error Handling", () => {
+ it("handles missing onClick gracefully", () => {
+ render(
+
+ Item without onClick
+
+ );
+
+ const item = screen.getByText("Item without onClick");
+ expect(item).toBeInTheDocument();
+ });
+
+ it("handles invalid props gracefully", () => {
+ render(
+
+
+ Item with invalid selected
+
+
+ );
+
+ const item = screen.getByText("Item with invalid selected");
+ expect(item).toBeInTheDocument();
+ });
+ });
+});
diff --git a/tests/integration/Input.integration.test.jsx b/tests/integration/Input.integration.test.jsx
index 0abc228..790960b 100644
--- a/tests/integration/Input.integration.test.jsx
+++ b/tests/integration/Input.integration.test.jsx
@@ -285,9 +285,9 @@ describe("Input Component Integration", () => {
// Set hover state
fireEvent.click(hoverButton);
- 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)]"
);
// Set active state
diff --git a/tests/integration/Select.integration.test.jsx b/tests/integration/Select.integration.test.jsx
new file mode 100644
index 0000000..3bc904f
--- /dev/null
+++ b/tests/integration/Select.integration.test.jsx
@@ -0,0 +1,407 @@
+import React, { useState } from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { expect, test, describe, it, vi } 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 (
+
+ );
+ };
+
+ describe("Form Integration", () => {
+ it("integrates with form submission", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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( );
+
+ 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( );
+
+ 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 (
+
+
+
+
+ );
+ };
+
+ it("handles multiple select components independently", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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( );
+
+ 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 (
+
+
+ setValues({ ...values, select1: value })}
+ options={[{ value: "a1", label: "A1" }]}
+ />
+
+ setValues({ ...values, select2: value })}
+ options={[{ value: "b1", label: "B1" }]}
+ />
+
+ );
+ };
+
+ it("handles keyboard navigation between inputs and selects", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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( );
+
+ 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 (
+
+ );
+ };
+
+ it("handles dynamic disabled state changes", async () => {
+ const { rerender } = render( );
+
+ const selectButton = screen.getByRole("button", {
+ name: /Dynamic Select/,
+ });
+ expect(selectButton).not.toBeDisabled();
+
+ rerender( );
+ expect(selectButton).toBeDisabled();
+
+ rerender( );
+ expect(selectButton).not.toBeDisabled();
+ });
+
+ it("handles dynamic error state changes", async () => {
+ const { rerender } = render( );
+
+ const selectButton = screen.getByRole("button", {
+ name: /Dynamic Select/,
+ });
+ expect(selectButton).not.toHaveClass(
+ "border-[var(--color-border-default-utility-negative)]"
+ );
+
+ rerender( );
+ expect(selectButton).toHaveClass(
+ "border-[var(--color-border-default-utility-negative)]"
+ );
+
+ rerender( );
+ expect(selectButton).not.toHaveClass(
+ "border-[var(--color-border-default-utility-negative)]"
+ );
+ });
+
+ it("handles dynamic size changes", async () => {
+ const { rerender } = render( );
+
+ const selectButton = screen.getByRole("button", {
+ name: /Dynamic Select/,
+ });
+ expect(selectButton).toHaveClass("h-[32px]");
+
+ rerender( );
+ expect(selectButton).toHaveClass("h-[36px]");
+
+ rerender( );
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ const selectButton = screen.getByRole("button", { name: /Test Select/ });
+
+ // Rapidly change props
+ for (let i = 0; i < 10; i++) {
+ rerender( );
+ 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(
+
+ );
+
+ 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();
+ });
+ });
+ });
+});
diff --git a/tests/unit/ContextMenu.test.jsx b/tests/unit/ContextMenu.test.jsx
new file mode 100644
index 0000000..1cc65a0
--- /dev/null
+++ b/tests/unit/ContextMenu.test.jsx
@@ -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( );
+
+ expect(screen.getByText("Context Menu Content")).toBeInTheDocument();
+ });
+
+ it("renders with custom className", () => {
+ render( );
+
+ const menu = screen.getByText("Context Menu Content").closest("div");
+ expect(menu).toHaveClass("custom-class");
+ });
+
+ it("applies correct base styles", () => {
+ render( );
+
+ 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( );
+
+ 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(
+
+ Menu Item
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper role", () => {
+ render( );
+
+ 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( );
+
+ expect(screen.getByText("Menu Item")).toBeInTheDocument();
+ });
+
+ it("renders as selected when selected prop is true", () => {
+ render( );
+
+ 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( );
+
+ // 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( );
+
+ // 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( );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveClass("text-[10px]", "leading-[14px]");
+ });
+
+ it("applies medium size styles", () => {
+ render( );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveClass("text-[14px]", "leading-[20px]");
+ });
+
+ it("applies large size styles", () => {
+ render( );
+
+ 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( );
+
+ 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( );
+
+ const item = screen.getByText("Menu Item");
+ await user.click(item);
+
+ expect(defaultProps.onClick).not.toHaveBeenCalled();
+ });
+
+ it("has hover effects", () => {
+ render( );
+
+ 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(
+
+
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper role", () => {
+ render( );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toBeInTheDocument();
+ });
+ });
+
+ describe("Styling", () => {
+ it("applies correct text color", () => {
+ render( );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveClass(
+ "text-[var(--color-content-default-brand-primary)]"
+ );
+ });
+
+ it("applies correct padding", () => {
+ render( );
+
+ const item = screen.getByRole("menuitem");
+ expect(item).toHaveClass("px-[8px]", "py-[4px]");
+ });
+
+ it("applies correct gap between checkmark and text", () => {
+ render( );
+
+ 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( );
+
+ expect(screen.getByText("Section Title")).toBeInTheDocument();
+ expect(screen.getByText("Section Content")).toBeInTheDocument();
+ });
+
+ it("renders without title when not provided", () => {
+ render(Section Content );
+
+ expect(screen.getByText("Section Content")).toBeInTheDocument();
+ expect(screen.queryByText("Section Title")).not.toBeInTheDocument();
+ });
+
+ it("applies correct title styling", () => {
+ render( );
+
+ 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( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ });
+});
+
+describe("ContextMenuDivider Component", () => {
+ describe("Rendering", () => {
+ it("renders divider", () => {
+ render( );
+
+ const divider = screen.getByRole("separator");
+ expect(divider).toBeInTheDocument();
+ });
+
+ it("applies correct styling", () => {
+ render( );
+
+ 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( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ });
+});
+
+describe("ContextMenu Components Integration", () => {
+ const TestMenu = () => (
+
+
+ Item 1
+
+ Item 2
+
+
+
+
+
+ Item 3
+
+
+
+ );
+
+ it("renders all components together", () => {
+ render( );
+
+ 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( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/tests/unit/Input.test.jsx b/tests/unit/Input.test.jsx
index 7aeb384..f52abf0 100644
--- a/tests/unit/Input.test.jsx
+++ b/tests/unit/Input.test.jsx
@@ -98,7 +98,7 @@ describe("Input Component", () => {
test("applies correct size classes", () => {
const { rerender } = render( );
let input = screen.getByRole("textbox");
- expect(input).toHaveClass("h-[30px]");
+ expect(input).toHaveClass("h-[32px]");
rerender( );
input = screen.getByRole("textbox");
@@ -146,9 +146,9 @@ describe("Input Component", () => {
test("applies hover state classes", () => {
render( );
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( );
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", () => {
diff --git a/tests/unit/Select.test.jsx b/tests/unit/Select.test.jsx
new file mode 100644
index 0000000..e96c3bd
--- /dev/null
+++ b/tests/unit/Select.test.jsx
@@ -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( );
+
+ expect(screen.getByText("Test Select")).toBeInTheDocument();
+ expect(screen.getByText("Select an option")).toBeInTheDocument();
+ });
+
+ it("renders without label when not provided", () => {
+ render(
+
+ );
+
+ expect(screen.queryByText("Test Select")).not.toBeInTheDocument();
+ expect(screen.getByText("Select an option")).toBeInTheDocument();
+ });
+
+ it("renders with horizontal label variant", () => {
+ render( );
+
+ const container = screen.getByText("Test Select").closest("div");
+ expect(container).toHaveClass("flex", "items-center");
+ });
+
+ it("renders with default label variant", () => {
+ render( );
+
+ const container = screen.getByText("Test Select").closest("div");
+ expect(container).toHaveClass("flex", "flex-col");
+ });
+ });
+
+ describe("Size Variants", () => {
+ it("renders small size correctly", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass("h-[32px]");
+ });
+
+ it("renders medium size correctly", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass("h-[36px]");
+ });
+
+ it("renders large size correctly", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass("h-[40px]");
+ });
+
+ it("applies correct height for small horizontal label", () => {
+ render(
+
+ );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass("h-[30px]");
+ });
+
+ it("applies correct height for small default label", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass("h-[32px]");
+ });
+ });
+
+ describe("State Variants", () => {
+ it("renders default state", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass(
+ "border-[var(--color-border-default-tertiary)]"
+ );
+ });
+
+ it("renders hover state", () => {
+ render( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass(
+ "shadow-[0_0_0_2px_var(--color-border-default-tertiary)]"
+ );
+ });
+
+ it("renders focus state", () => {
+ render( );
+
+ 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( );
+
+ const selectButton = screen.getByRole("button");
+ expect(selectButton).toHaveClass(
+ "border-[var(--color-border-default-utility-negative)]"
+ );
+ });
+
+ it("renders disabled state", () => {
+ render( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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( );
+
+ 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(
+
+ );
+
+ 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( );
+
+ expect(screen.getByText("Select an option")).toBeInTheDocument();
+ });
+
+ it("shows selected value when option is selected", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ 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( );
+
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
+ });
+ });
+
+ describe("Accessibility", () => {
+ it("has no accessibility violations", async () => {
+ const { container } = render( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has proper ARIA attributes", () => {
+ render( );
+
+ 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( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ await waitFor(() => {
+ expect(selectButton).toHaveAttribute("aria-expanded", "true");
+ });
+ });
+
+ it("associates label with select button", () => {
+ render( );
+
+ 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( );
+
+ 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( );
+
+ const selectButton = screen.getByRole("button");
+ await user.click(selectButton);
+
+ expect(selectButton).toHaveFocus();
+ // Focus state should not be applied on click, only on keyboard navigation
+ });
+ });
+});