+
{children}
),
@@ -261,6 +268,16 @@ describe("SidebarDraggableComponent", () => {
mockDeleteFlow.mockClear();
});
+ describe("Accessibility", () => {
+ it("should_have_no_axe_violations", async () => {
+ const { container } = render(
+
,
+ );
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+ });
+
describe("Basic Rendering", () => {
it("should render component with correct structure", () => {
render(
);
@@ -474,15 +491,11 @@ describe("SidebarDraggableComponent", () => {
);
});
- it("should call addComponent when Enter key is pressed on the add button", async () => {
- const user = userEvent.setup();
+ it("should call addComponent when Enter key is pressed", () => {
render(
);
- const addButton = screen.getByTestId(
- "add-component-button-test-component",
- );
- addButton.focus();
- await user.keyboard("{Enter}");
+ const container = screen.getByTestId(/testsectiontest component/i);
+ fireEvent.keyDown(container, { key: "Enter" });
expect(mockAddComponentFn).toHaveBeenCalledWith(
mockAPIClass,
@@ -490,15 +503,11 @@ describe("SidebarDraggableComponent", () => {
);
});
- it("should call addComponent when Space key is pressed on the add button", async () => {
- const user = userEvent.setup();
+ it("should call addComponent when Space key is pressed", () => {
render(
);
- const addButton = screen.getByTestId(
- "add-component-button-test-component",
- );
- addButton.focus();
- await user.keyboard(" ");
+ const container = screen.getByTestId(/testsectiontest component/i);
+ fireEvent.keyDown(container, { key: " " });
expect(mockAddComponentFn).toHaveBeenCalledWith(
mockAPIClass,
@@ -506,15 +515,11 @@ describe("SidebarDraggableComponent", () => {
);
});
- it("should not call addComponent for other keys", async () => {
- const user = userEvent.setup();
+ it("should not call addComponent for other keys", () => {
render(
);
- const addButton = screen.getByTestId(
- "add-component-button-test-component",
- );
- addButton.focus();
- await user.keyboard("{Escape}");
+ const container = screen.getByTestId(/testsectiontest component/i);
+ fireEvent.keyDown(container, { key: "Escape" });
expect(mockAddComponentFn).not.toHaveBeenCalled();
});
@@ -568,39 +573,40 @@ describe("SidebarDraggableComponent", () => {
expect(draggableDiv).toHaveStyle({ borderLeftColor: "#FF0000" });
});
- it("should not expose widget semantics on the draggable container", () => {
+ it("should have correct tabIndex", () => {
render(
);
- const container = screen.getByTestId(
- "testsection_test component_draggable",
- );
- expect(container).not.toHaveAttribute("role");
- expect(container).not.toHaveAttribute("tabIndex");
- expect(container).not.toHaveAttribute("aria-label");
+ const container = screen.getByTestId(/testsectiontest component/i);
+ expect(container).toHaveAttribute("tabIndex", "0");
});
- it("should expose the accessible name on the add button only", () => {
+ it("should expose role=button and an accessible name on the draggable content, not the outer wrapper", () => {
render(
);
- const addButton = screen.getByTestId(
- "add-component-button-test-component",
- );
- expect(addButton).toHaveAttribute(
+ const content = screen.getByTestId(/testsectiontest component/i);
+ expect(content).toHaveAttribute("role", "button");
+ expect(content).toHaveAttribute(
"aria-label",
"Add Test Component to canvas",
);
- expect(
- screen.getAllByLabelText("Add Test Component to canvas"),
- ).toHaveLength(1);
+
+ // The outer wrapper must NOT carry role=button, since it also contains
+ // the SelectTrigger (role=combobox) — an interactive descendant is
+ // invalid inside a button-role element (IBM aria_descendant_valid).
+ const outerWrapper = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ expect(outerWrapper).not.toHaveAttribute("role");
+ expect(outerWrapper).not.toHaveAttribute("tabIndex");
});
- it("should keep the add button keyboard-focusable", () => {
+ it("should have add button with tabIndex -1", () => {
render(
);
const addButton = screen.getByTestId(
"add-component-button-test-component",
);
- expect(addButton).not.toHaveAttribute("tabIndex");
+ expect(addButton).toHaveAttribute("tabIndex", "-1");
});
it("should have select trigger with tabIndex -1", () => {
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.a11y.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.a11y.test.tsx
new file mode 100644
index 000000000000..1e346146bd24
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.a11y.test.tsx
@@ -0,0 +1,87 @@
+import { render, screen } from "@testing-library/react";
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { axe } from "@/utils/a11y-test";
+import type { SidebarHeaderComponentProps } from "../../types";
+import { SidebarHeaderComponent } from "../sidebarHeader";
+
+// sidebarHeader.test.tsx mocks Button/ShadTooltip/sidebar/disclosure for
+// interaction testing. This suite renders the real Button, ShadTooltip, and
+// @/components/ui/sidebar primitives to check the actual DOM/ARIA output —
+// child sections (FeatureToggles, SearchInput, SidebarFilterComponent) have
+// their own a11y coverage, so they stay mocked here.
+jest.mock("../featureTogglesComponent", () => ({
+ __esModule: true,
+ default: () =>
,
+}));
+
+jest.mock("../searchInput", () => ({
+ SearchInput: () =>
,
+}));
+
+jest.mock("../sidebarFilterComponent", () => ({
+ SidebarFilterComponent: () =>
,
+}));
+
+jest.mock("@/customization/feature-flags", () => ({
+ ENABLE_NEW_SIDEBAR: false,
+}));
+
+// The global jest.setup.js mock for genericIconComponent only stubs the
+// default export, not the named ForwardedIconComponent this component uses.
+jest.mock("@/components/common/genericIconComponent", () => ({
+ __esModule: true,
+ default: () => null,
+ ForwardedIconComponent: ({ name }: { name?: string }) => (
+
+ ),
+}));
+
+const defaultProps: SidebarHeaderComponentProps = {
+ showConfig: false,
+ setShowConfig: jest.fn(),
+ showBeta: false,
+ setShowBeta: jest.fn(),
+ showLegacy: false,
+ setShowLegacy: jest.fn(),
+ searchInputRef: { current: null },
+ isInputFocused: false,
+ search: "",
+ handleInputFocus: jest.fn(),
+ handleInputBlur: jest.fn(),
+ handleInputChange: jest.fn(),
+ filterName: "",
+ filterDescription: "",
+ resetFilters: jest.fn(),
+};
+
+const renderHeader = (props: Partial
= {}) =>
+ render(
+
+
+
+
+ ,
+ );
+
+describe("SidebarHeaderComponent accessibility (real Button/ShadTooltip/sidebar, unmocked)", () => {
+ it("should_have_no_axe_violations when config is closed", async () => {
+ const { container } = renderHeader();
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it("should_have_no_axe_violations when config is open", async () => {
+ const { container } = renderHeader({ showConfig: true });
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it("names the settings trigger button", () => {
+ renderHeader();
+
+ expect(screen.getByTestId("sidebar-options-trigger")).toHaveAccessibleName(
+ "Component settings",
+ );
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx
index fe8f56de6f6a..4266eee49f44 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx
@@ -162,7 +162,6 @@ describe("SidebarHeaderComponent", () => {
expect(
screen.getByTestId("sidebar-options-trigger"),
).toBeInTheDocument();
- expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
expect(screen.getByTestId("search-input")).toBeInTheDocument();
expect(screen.getByTestId("disclosure")).toBeInTheDocument();
expect(screen.getByTestId("disclosure-content")).toBeInTheDocument();
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarSegmentedNav.a11y.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarSegmentedNav.a11y.test.tsx
new file mode 100644
index 000000000000..7f7842f6abb1
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarSegmentedNav.a11y.test.tsx
@@ -0,0 +1,36 @@
+import { render } from "@testing-library/react";
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { axe } from "@/utils/a11y-test";
+import SidebarSegmentedNav from "../sidebarSegmentedNav";
+
+// sidebarSegmentedNav.test.tsx mocks @/components/ui/sidebar and ShadTooltip
+// entirely for interaction testing. This suite renders the real sidebar
+// primitives and ShadTooltip to check actual DOM/ARIA output.
+const mockSetSearch = jest.fn();
+jest.mock("../../index", () => ({
+ useSearchContext: () => ({
+ focusSearch: jest.fn(),
+ isSearchFocused: false,
+ setSearch: mockSetSearch,
+ }),
+}));
+
+jest.mock("@/stores/playgroundStore", () => ({
+ usePlaygroundStore: (selector: (state: Record) => unknown) =>
+ selector({ setIsOpen: jest.fn(), setIsFullscreen: jest.fn() }),
+}));
+
+const renderNav = () =>
+ render(
+
+
+ ,
+ );
+
+describe("SidebarSegmentedNav accessibility (real sidebar primitives/ShadTooltip, unmocked)", () => {
+ it("should_have_no_axe_violations", async () => {
+ const { container } = renderNav();
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx
index fe6cffdf4ddf..1fde59d08efa 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx
@@ -15,7 +15,11 @@ export const SearchConfigTrigger = ({
const { t } = useTranslation();
return (
-
+
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput.tsx
index 93d901f81eb7..3037b55e69ff 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput.tsx
@@ -29,6 +29,7 @@ export const SearchInput = memo(function SearchInput({
data-testid="sidebar-search-input"
inputClassName="w-full rounded-lg bg-background text-sm"
placeholder={t("sidebar.searchPlaceholder")}
+ aria-label={t("sidebar.searchAriaLabel")}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onChange={handleInputChange}
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx
index d69d1845b847..d090db6ef3f6 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx
@@ -104,6 +104,14 @@ export const SidebarDraggableComponent = forwardRef(
}
}
+ const handleKeyDown = (e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ e.stopPropagation();
+ addComponent(apiClass, itemName);
+ }
+ };
+
return (
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
index 7c33be61f4dd..f8f0079e1d3a 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
@@ -3,11 +3,7 @@ import { useTranslation } from "react-i18next";
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Button } from "@/components/ui/button";
-import {
- Disclosure,
- DisclosureContent,
- DisclosureTrigger,
-} from "@/components/ui/disclosure";
+import { Disclosure, DisclosureContent } from "@/components/ui/disclosure";
import { SidebarHeader, SidebarTrigger } from "@/components/ui/sidebar";
import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import type { SidebarHeaderComponentProps } from "../types";
@@ -44,26 +40,25 @@ export const SidebarHeaderComponent = memo(function SidebarHeaderComponent({
{t("sidebar.components")}
-
-
-
-
-
-
-
+
+
+