-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(a11y): Add ARIA attributes to Button and Select components #41657
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| import React from "react"; | ||
| import { render, screen, fireEvent, waitFor } from "@testing-library/react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| import "@testing-library/jest-dom"; | ||
| import { ThemeProvider } from "styled-components"; | ||
| import { lightTheme } from "selectors/themeSelectors"; | ||
| import { Collapsible, CollapsibleGroup, DisabledCollapsible } from "./Collapsible"; | ||
|
|
||
| const renderCollapsible = (props = {}) => { | ||
| return render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <Collapsible label="Test Collapsible" {...props}> | ||
| <div data-testid="collapsible-content">Collapsible Content</div> | ||
| </Collapsible> | ||
| </ThemeProvider>, | ||
| ); | ||
| }; | ||
|
|
||
| describe("Collapsible Accessibility Tests", () => { | ||
| describe("ARIA Attributes", () => { | ||
| it("should render the collapsible component with a label", () => { | ||
| renderCollapsible(); | ||
|
|
||
| expect(screen.getByText("Test Collapsible")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should render children when expanded by default", () => { | ||
| renderCollapsible({ expand: true }); | ||
|
|
||
| expect(screen.getByTestId("collapsible-content")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should have an expandable icon that indicates state", () => { | ||
| const { container } = renderCollapsible({ expand: true }); | ||
|
|
||
| const icon = container.querySelector(".collapsible-icon"); | ||
| expect(icon).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should toggle expansion state when clicking the label", async () => { | ||
| renderCollapsible({ expand: false }); | ||
|
|
||
| // Initially collapsed | ||
| expect(screen.queryByTestId("collapsible-content")).not.toBeInTheDocument(); | ||
|
|
||
| // Click to expand | ||
| const label = screen.getByText("Test Collapsible").closest(".icon-text"); | ||
| if (label) { | ||
| fireEvent.click(label); | ||
| } | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByTestId("collapsible-content")).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| it("should collapse when clicking the label on expanded state", async () => { | ||
| renderCollapsible({ expand: true }); | ||
|
|
||
| // Initially expanded | ||
| expect(screen.getByTestId("collapsible-content")).toBeInTheDocument(); | ||
|
|
||
| // Click to collapse | ||
| const label = screen.getByText("Test Collapsible").closest(".icon-text"); | ||
| if (label) { | ||
| fireEvent.click(label); | ||
| } | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.queryByTestId("collapsible-content")).not.toBeInTheDocument(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Keyboard Accessibility", () => { | ||
| it("should have a clickable label element", () => { | ||
| const { container } = renderCollapsible(); | ||
|
|
||
| const label = container.querySelector(".icon-text"); | ||
| expect(label).toBeInTheDocument(); | ||
| expect(label).toHaveStyle({ cursor: "pointer" }); | ||
| }); | ||
|
|
||
| it("should be accessible via click interaction", async () => { | ||
| const handleCustomCollapse = jest.fn(); | ||
| renderCollapsible({ handleCustomCollapse }); | ||
|
|
||
| const label = screen.getByText("Test Collapsible").closest(".icon-text"); | ||
| if (label) { | ||
| fireEvent.click(label); | ||
| } | ||
|
|
||
| await waitFor(() => { | ||
| expect(handleCustomCollapse).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Icon State Indication", () => { | ||
| it("should show down-arrow icon when expanded", () => { | ||
| const { container } = renderCollapsible({ expand: true }); | ||
|
|
||
| const icon = container.querySelector(".collapsible-icon"); | ||
| expect(icon).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should show arrow-right icon when collapsed", () => { | ||
| const { container } = renderCollapsible({ expand: false }); | ||
|
|
||
| const icon = container.querySelector('[name="arrow-right-s-line"]'); | ||
| expect(icon).toBeInTheDocument(); | ||
| }); | ||
|
Comment on lines
+107
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check how Icon component renders the name prop
ast-grep --pattern $'function Icon($$$) {
$$$
}'
rg -n "name.*arrow-right" --type=tsx -A3 -B3Repository: appsmithorg/appsmith Length of output: 8759 🏁 Script executed: cat -n app/client/packages/design-system/ads/src/Icon/Icon.provider.tsxRepository: appsmithorg/appsmith Length of output: 50376 🏁 Script executed: # Check the Collapsible component to see how it uses Icon
cat -n app/client/src/components/common/Collapsible.tsx | sed -n '130,150p'Repository: appsmithorg/appsmith Length of output: 772 🏁 Script executed: # Search for other test usage of [name="..."] selectors to see if this pattern is used elsewhere
rg '\[name=' app/client/src/components/common --type=tsx -B2 -A2Repository: appsmithorg/appsmith Length of output: 91 🏁 Script executed: rg "arrow-right-s-line" app/client/packages/design-system/ads/src/Icon/Icon.provider.tsxRepository: appsmithorg/appsmith Length of output: 108 🏁 Script executed: # Find and read the IconProvider implementation
find app/client/packages/design-system/ads/src/Icon -name "*.tsx" -o -name "*.ts" | head -20Repository: appsmithorg/appsmith Length of output: 680 🏁 Script executed: # Search for other test files using [name=" selector pattern
rg '\[name=' app/client --type=tsx | head -20Repository: appsmithorg/appsmith Length of output: 91 🏁 Script executed: cat -n app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx | head -100Repository: appsmithorg/appsmith Length of output: 4080 🏁 Script executed: # Search for how tests query icon elements with [name= pattern
rg 'querySelector.*name=' app/client/src --type=tsx -B3 -A3 | head -50Repository: appsmithorg/appsmith Length of output: 91 🏁 Script executed: grep -r '\[name=' app/client/src --include="*.tsx" --include="*.ts" | grep querySelector | head -30Repository: appsmithorg/appsmith Length of output: 729 🏁 Script executed: # Look at the Icon test file to see how it tests icon rendering
cat -n app/client/packages/design-system/ads/src/Icon/Icon.test.tsxRepository: appsmithorg/appsmith Length of output: 1632 🏁 Script executed: # Get the IconProvider component implementation
sed -n '1200,1300p' app/client/packages/design-system/ads/src/Icon/Icon.provider.tsxRepository: appsmithorg/appsmith Length of output: 3279 🏁 Script executed: # Search for the IconProvider export/component definition
rg "export.*IconProvider\|const IconProvider\|function IconProvider" app/client/packages/design-system/ads/src/Icon/Repository: appsmithorg/appsmith Length of output: 46 🏁 Script executed: # Check if SVG or icon elements have a name attribute in the render
rg -A5 "return.*name\|\.name\s*=" app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx | head -40Repository: appsmithorg/appsmith Length of output: 46 🏁 Script executed: # Find where IconProvider is defined and how it renders
rg "IconProvider" app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx | grep -E "export|function|const" | head -20Repository: appsmithorg/appsmith Length of output: 101 🏁 Script executed: # Look at the end of Icon.provider.tsx file to see the export/implementation
tail -100 app/client/packages/design-system/ads/src/Icon/Icon.provider.tsxRepository: appsmithorg/appsmith Length of output: 2609 🏁 Script executed: # Check how remixicon-react SVG icons render - look at loadables to see if name attr is added
cat -n app/client/packages/design-system/ads/src/Icon/loadables.tsxRepository: appsmithorg/appsmith Length of output: 2209 🏁 Script executed: # Look at the Accordion test to see if it's also failing or if there's a pattern
cat -n app/client/src/pages/AdminSettings/FormGroup/Accordion.test.tsx | sed -n '1,100p'Repository: appsmithorg/appsmith Length of output: 3441 🏁 Script executed: # Check the complete Icon.tsx to see if name attribute is added anywhere
cat -n app/client/packages/design-system/ads/src/Icon/Icon.tsxRepository: appsmithorg/appsmith Length of output: 1219 🏁 Script executed: # Check if there's any test setup or utility that adds name attributes
rg "name=" app/client/src/components/common/Collapsible.test.tsx -B5 -A5Repository: appsmithorg/appsmith Length of output: 675 🏁 Script executed: # Look at the full Collapsible.test.tsx file to understand the test
cat -n app/client/src/components/common/Collapsible.test.tsxRepository: appsmithorg/appsmith Length of output: 11250 Selector will not match rendered icon element. The selector 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| describe("State Management", () => { | ||
| it("should respect initial expand prop", () => { | ||
| renderCollapsible({ expand: true }); | ||
|
|
||
| expect(screen.getByTestId("collapsible-content")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should respect initial collapsed state", () => { | ||
| renderCollapsible({ expand: false }); | ||
|
|
||
| expect(screen.queryByTestId("collapsible-content")).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should update state when expand prop changes", async () => { | ||
| const { rerender } = render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <Collapsible label="Test" expand={false}> | ||
| <div data-testid="content">Content</div> | ||
| </Collapsible> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| expect(screen.queryByTestId("content")).not.toBeInTheDocument(); | ||
|
|
||
| rerender( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <Collapsible label="Test" expand={true}> | ||
| <div data-testid="content">Content</div> | ||
| </Collapsible> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByTestId("content")).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("CustomLabelComponent", () => { | ||
| it("should render custom label component when provided", () => { | ||
| const CustomLabel = ({ datasource }: { datasource?: unknown }) => ( | ||
| <span data-testid="custom-label">Custom Label</span> | ||
| ); | ||
|
|
||
| renderCollapsible({ CustomLabelComponent: CustomLabel }); | ||
|
|
||
| expect(screen.getByTestId("custom-label")).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("DisabledCollapsible", () => { | ||
| it("should render with disabled styling", () => { | ||
| const { container } = render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <DisabledCollapsible label="Disabled Section" /> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| expect(screen.getByText("Disabled Section")).toBeInTheDocument(); | ||
|
|
||
| // Check for disabled styling | ||
| const wrapper = container.querySelector('[class*="CollapsibleWrapper"]'); | ||
| expect(wrapper).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should show tooltip when provided", () => { | ||
| render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <DisabledCollapsible | ||
| label="Disabled Section" | ||
| tooltipLabel="This section is disabled" | ||
| /> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| expect(screen.getByText("Disabled Section")).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("CollapsibleGroup", () => { | ||
| it("should render children within a group", () => { | ||
| render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <CollapsibleGroup> | ||
| <div data-testid="group-child">Group Child</div> | ||
| </CollapsibleGroup> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| expect(screen.getByTestId("group-child")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should apply height and maxHeight props", () => { | ||
| const { container } = render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <CollapsibleGroup height="500px" maxHeight="600px"> | ||
| <div>Content</div> | ||
| </CollapsibleGroup> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| const wrapper = container.firstChild; | ||
| expect(wrapper).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Accessibility Recommendations", () => { | ||
| /** | ||
| * Note: The Collapsible component currently lacks some accessibility attributes | ||
| * that would improve screen reader support: | ||
| * | ||
| * - aria-expanded: Should be "true" when expanded, "false" when collapsed | ||
| * - aria-controls: Should reference the ID of the content panel | ||
| * - role="button": The clickable label should have this role | ||
| * - tabIndex="0": To make it keyboard focusable | ||
| * - Keyboard support: Enter and Space keys should toggle the collapse state | ||
| * | ||
| * These tests document the expected behavior for accessibility compliance. | ||
| */ | ||
|
|
||
| it("should have a label that is clickable", () => { | ||
| const { container } = renderCollapsible(); | ||
|
|
||
| const label = container.querySelector(".icon-text"); | ||
| expect(label).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should provide visual indication of expanded/collapsed state", () => { | ||
| const { container, rerender } = render( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <Collapsible label="Test" expand={true}> | ||
| <div>Content</div> | ||
| </Collapsible> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| // Check that icon indicates expanded state | ||
| let icon = container.querySelector(".collapsible-icon"); | ||
| expect(icon).toBeInTheDocument(); | ||
|
|
||
| // Re-render with collapsed state | ||
| rerender( | ||
| <ThemeProvider theme={lightTheme}> | ||
| <Collapsible label="Test" expand={false}> | ||
| <div>Content</div> | ||
| </Collapsible> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| // Icon should still be present but may have different attributes | ||
| icon = container.querySelector('[name="arrow-right-s-line"]'); | ||
| expect(icon).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Handle Custom Collapse", () => { | ||
| it("should call handleCustomCollapse when toggle occurs", async () => { | ||
| const handleCustomCollapse = jest.fn(); | ||
| renderCollapsible({ handleCustomCollapse, expand: true }); | ||
|
|
||
| const label = screen.getByText("Test Collapsible").closest(".icon-text"); | ||
| if (label) { | ||
| fireEvent.click(label); | ||
| } | ||
|
|
||
| await waitFor(() => { | ||
| expect(handleCustomCollapse).toHaveBeenCalledWith(false); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent test skip on missing element.
If
.closest(".icon-text")returnsnull, the assertion inside theifblock is silently skipped, and the test passes without actually clicking anything. This can mask regressions.Consider throwing or asserting the element exists:
const label = screen.getByText("Test Collapsible").closest(".icon-text"); - if (label) { - fireEvent.click(label); - } + expect(label).not.toBeNull(); + fireEvent.click(label!);Same pattern applies to lines 64-67, 88-91, and 275-278.
📝 Committable suggestion
🤖 Prompt for AI Agents