Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions app/generator/components/EditorPanel.accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ describe('EditorPanel Component Accessibility Tests', () => {
expect(form).toBeInTheDocument();

// Section card heading hierarchy (h3) and regions
const nameHeading = screen.getByRole('heading', { level: 3, name: /^name$/i });
expect(nameHeading).toBeInTheDocument();

const nameRegion = screen.getByRole('region', { name: /^name$/i });
expect(nameRegion).toBeInTheDocument();
const headingId = nameHeading.getAttribute('id');
expect(headingId).toBeTruthy();
expect(nameRegion).toHaveAttribute('aria-labelledby', headingId!);

const headerButton = screen.getByRole('button', {
name: /your display name for the readme header/i,
});

const nameRegion = screen.getByRole('region', {
name: /your display name for the readme header/i,
});

expect(nameRegion).toHaveAttribute('aria-labelledby', headerButton.id);
});

// Test 2: Label to Input Pairings
Expand Down Expand Up @@ -131,15 +133,17 @@ describe('EditorPanel Component Accessibility Tests', () => {
it('5. verifies that the SectionCard header updates aria-expanded states correctly when toggled', () => {
render(<EditorPanel state={mockState} {...mockHandlers} />);

const headerButton = screen.getByRole('button', { name: /^name$/i });
const headerButton = screen.getByRole('button', {
name: /your display name for the readme header/i,
});
expect(headerButton).toHaveAttribute('aria-expanded', 'true');

// Collapse panel
fireEvent.click(headerButton);
expect(headerButton).toHaveAttribute('aria-expanded', 'false');

// Confirm that region content panel is removed or hidden
const nameRegion = screen.queryByRole('region', { name: /^name$/i });
const nameRegion = screen.queryByRole('region', { name: /name/i });
expect(nameRegion).toBeNull();
});
});
56 changes: 2 additions & 54 deletions app/generator/components/SectionCard.accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,6 @@ import { describe, expect, it } from 'vitest';
import { SectionCard, FieldLabel } from './SectionCard';

describe('SectionCard Accessibility', () => {
/**
* Test Case 1: ARIA & Accessible Markup Validation
*
* Verifies that the interactive controls are identified correctly by screen readers,
* and validates the presence of expected roles and labels.
*
* NOTE:
* During testing, we found that SectionCard lacks standard dynamic ARIA properties.
* TODO: Implement the following improvements in SectionCard.tsx:
* - Add `aria-expanded={open}` on the main toggle button.
* - Add `aria-controls={contentId}` pointing to the collapsible content region.
* - Add `role="region"` or `role="group"` with `aria-labelledby` referencing the button/title to the children wrapper.
* - Add `aria-describedby` referencing the description paragraph.
*/
it('1. ARIA & Accessible Markup Validation: verifies interactive elements expose correct accessibility attributes', () => {
// Arrange
const titleText = 'Profile Details';
Expand Down Expand Up @@ -64,9 +50,8 @@ describe('SectionCard Accessibility', () => {
// Verify the content region exists, carries the correct role, ID, and label
const contentRegion = screen.getByRole('region', { name: new RegExp(titleText, 'i') });
expect(contentRegion).toHaveAttribute('id', contentId!);

const titleElement = screen.getByText(titleText);
expect(titleElement).toHaveAttribute('id', contentRegion.getAttribute('aria-labelledby')!);
expect(descElement.id).toBe(descriptionId);
expect(contentRegion).toHaveAttribute('aria-labelledby', toggleButton.id);

// Verify children containing fields using ARIA attributes map correctly
const textInput = screen.getByRole('textbox', { name: 'Full Name' });
Expand All @@ -77,12 +62,6 @@ describe('SectionCard Accessibility', () => {
expect(helperText).toBeInTheDocument();
});

/**
* Test Case 2: Keyboard Focus Visibility
*
* Verifies focusable controls receive focus via keyboard tab navigation, and that
* the custom ring/focus outline styles are properly applied without losing focus.
*/
it('2. Keyboard Focus Visibility: verifies focusable controls receive focus and focus indicators are preserved', async () => {
// Arrange
const user = userEvent.setup();
Expand Down Expand Up @@ -112,16 +91,7 @@ describe('SectionCard Accessibility', () => {
expect(document.body).toHaveFocus();
});

/**
* Test Case 3: Tooltip Accessibility
*
* Verifies tooltip triggers inside SectionCard expose accessible descriptions,
* and that screen readers can announce the tooltip contents via correct relations.
*/
it('3. Tooltip Accessibility: verifies tooltip trigger associates with content via ARIA attributes', () => {
// Arrange & Act
// Render a mock tooltip implementation in SectionCard's children to verify
// the wrapper component handles accessible tooltip patterns correctly.
render(
<SectionCard title="Tooltip Integration Section">
<div>
Expand All @@ -141,18 +111,8 @@ describe('SectionCard Accessibility', () => {
// Assert
expect(trigger).toHaveAttribute('aria-describedby', 'info-tooltip');
expect(tooltip).toHaveTextContent('This explains the section settings in detail.');

// TODO: Standardize tooltips inside SectionCard by packaging a reusable,
// fully compliant tooltip component in the generator components directory.
});

/**
* Test Case 4: Keyboard Navigation Order
*
* Verifies the tab sequence flows in a natural order across interactive controls.
* Also ensures that when the SectionCard is collapsed, the child inputs
* are unmounted and therefore skipped entirely in keyboard navigation.
*/
it('4. Keyboard Navigation Order: verifies sequential keyboard navigation and that closed card controls are unreachable', async () => {
// Arrange
const user = userEvent.setup();
Expand Down Expand Up @@ -206,16 +166,7 @@ describe('SectionCard Accessibility', () => {
expect(document.body).toHaveFocus();
});

/**
* Test Case 5: Heading Hierarchy Validation
*
* Verifies that the SectionCard title uses a semantic heading level (h3)
* to align with screen reader expectations and allow landmark navigation.
* Also verifies headings inside children follow proper layout nesting rules.
*/
it('5. Heading Hierarchy Validation: verifies heading structure logical flow and checks for violations', () => {
// Arrange & Act
// Render SectionCard alongside correct heading structures in children
render(
<SectionCard title="Card Title Content">
<section>
Expand All @@ -239,8 +190,5 @@ describe('SectionCard Accessibility', () => {

expect(h2El).toHaveTextContent('Primary Section Level 2 Heading');
expect(h3El).toHaveTextContent('Secondary Subsection Level 3 Heading');

// TODO: Consider introducing a heading-level prop (e.g. `titleAs="h2" | "h3" | "span"`)
// to allow callers to align the SectionCard header title semantically with page layout outlines.
});
});
3 changes: 1 addition & 2 deletions app/generator/components/SectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function SectionCard({
id={headerId}
aria-expanded={open}
aria-controls={contentId}
aria-labelledby={titleId}
aria-describedby={description ? descriptionId : undefined}
onClick={() => setOpen((p) => !p)}
className="w-full flex items-center gap-3 px-5 py-4 text-left group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/50"
Expand Down Expand Up @@ -68,7 +67,7 @@ export function SectionCard({
</button>

{open && (
<div id={contentId} role="region" aria-labelledby={titleId} className="px-5 pb-5 pt-1">
<div id={contentId} role="region" aria-labelledby={headerId} className="px-5 pb-5 pt-1">
<div className="h-px bg-gray-100 dark:bg-white/5 mb-4" />
{children}
</div>
Expand Down
Loading