How do I test Accessibility Actions in React Native?
Introduction
In the previous post, we covered the fundamentals of accessibilityActions and implemented a slider and sortable list. Now we'll explore how to test these implementations programmatically using React Native Testing Library, along with best practices and platform considerations.
Testing with React Native Testing Library
The best way to test accessibility actions is programmatically using React Native Testing Library. This approach lets you write automated tests that run in CI/CD, catching issues before they reach production.
Basic Testing Pattern
Use fireEvent to trigger accessibility actions in your tests:
import { render, screen, fireEvent } from '@testing-library/react-native';
let volumeUpEvent = { actionName: "increment" };
let volumeDownEvent = { actionName: "decrement" };
it("adjusts volume with accessibility actions", () => {
render(<VolumeSlider />);
let volumeSlider = screen.getByRole("adjustable");
// Verify initial state
expect(volumeSlider).toHaveAccessibilityValue({
max: 100,
min: 0,
now: 50,
});
// Trigger increment action
fireEvent(volumeSlider, "onAccessibilityAction", {
nativeEvent: volumeUpEvent,
});
// Verify updated state
expect(volumeSlider).toHaveAccessibilityValue({
max: 100,
min: 0,
now: 60,
});
});
Testing Multiple Actions
For components with multiple accessibility actions, test each action independently:
let toggleEvent = { actionName: "activate" };
let upEvent = { actionName: "up" };
let downEvent = { actionName: "down" };
it("handles multiple accessibility actions", () => {
let setMenuItem = jest.fn();
render(<MenuItem id="item-1" title="Favorites" setMenuItem={setMenuItem} />);
let menuItem = screen.getByText("Favorites");
// Test toggle action
fireEvent(menuItem, "onAccessibilityAction", {
nativeEvent: toggleEvent,
});
expect(setMenuItem).toHaveBeenCalledWith("item-1", "toggle");
// Test move up action
fireEvent(menuItem, "onAccessibilityAction", {
nativeEvent: upEvent,
});
expect(setMenuItem).toHaveBeenCalledWith("item-1", "up");
});
Working Example Repository
For complete working examples with full test coverage, check out the accessibility-actions repository on GitHub. This repo includes:
- Tested slider component with increment/decrement actions (Controls.spec.tsx)
- Tested sortable list with multiple actions (MyMenuEdit.spec.tsx)
- CI/CD integration running tests on every commit
- Implementation examples matching the patterns from the previous post
Clone and run the tests locally:
git clone https://github.com/dtun/accessibility-actions.git
cd accessibility-actions
npm install
npm test
Best Practices
Provide Clear Action Names
Action names should be concise and descriptive. Screen readers announce these names to users, so clarity is essential:
// Good: Clear and actionable
{ name: "activate", label: "Toggle checked state" }
{ name: "up", label: "Move item up" }
{ name: "increment", label: "Increase volume" }
// Avoid: Vague or technical
{ name: "action1", label: "Do thing" }
{ name: "handler", label: "Execute" }
Make Actions Discoverable
Users need to know actions exist. Use accessibilityHint to guide users:
// Sortable menu item with hint
function MenuItem({ id, title, checked, setMenuItem }) {
return (
<Pressable
accessible
accessibilityLabel={title}
// Hint tells users how to discover more actions
accessibilityHint="Double tap to toggle, swipe up or down for more actions"
accessibilityActions={[toggleAction, upAction, downAction]}
accessibilityState={{ checked }}
onAccessibilityAction={onAccessibilityAction}
onPress={()=> setMenuItem(id, "toggle")}
>
<Text accessible={false}>{title}</Text>
</Pressable>
);
}
The hint tells users:
- Primary action (double tap)
- Additional actions are available (swipe gestures)
Combine with Appropriate Roles
The accessibilityRole prop tells assistive technologies what type of element users are interacting with:
// For sliders and adjustable controls
function VolumeSlider() {
return (
<View
// Use "adjustable" role for increment/decrement actions
accessibilityRole="adjustable"
accessibilityActions={[incrementAction, decrementAction]}
onAccessibilityAction={handleAction}
>
<Slider />
</View>
);
}
Define Actions as Constants
Define your actions once and reuse them throughout your component:
// Define at the top of your file
let incrementAction = { name: "increment", label: "increment" };
let decrementAction = { name: "decrement", label: "decrement" };
// Or for list items
let toggleAction = { name: "activate", label: "Toggle checked state" };
let upAction = { name: "up", label: "Move item up" };
let downAction = { name: "down", label: "Move item down" };
Platform Considerations
How Users Invoke Actions
Both iOS and Android handle the same accessibilityActions array, but users invoke them differently:
iOS VoiceOver:
- Swipe up/down with one finger to cycle through actions
- Open the rotor (two-finger rotation) and select "Actions" menu
- Voice Control users can speak action names directly
Android TalkBack:
- Access actions through the local context menu (long press or swipe up then right)
- Actions appear in the reading controls menu
Your programmatic tests verify the actions work correctly regardless of how users invoke them on each platform.
Testing Edge Cases
Beyond the basic testing patterns, verify edge cases in your tests:
it("caps volume at maximum", () => {
render(<VolumeSlider initialVolume={95} />);
let volumeSlider = screen.getByRole("adjustable");
// Increment beyond max
fireEvent(volumeSlider, "onAccessibilityAction", {
nativeEvent: { actionName: "increment" },
});
fireEvent(volumeSlider, "onAccessibilityAction", {
nativeEvent: { actionName: "increment" },
});
// Should cap at 100
expect(volumeSlider).toHaveAccessibilityValue({
max: 100,
min: 0,
now: 100,
});
});
Test other edge cases:
- Boundary conditions: Min/max values are properly enforced
- Multiple instances: Each instance manages its own state correctly
- Rapid actions: Triggering actions in quick succession works as expected
Implementation Patterns from the Repository
The accessibility-actions repository demonstrates several key patterns you should follow.
Always Provide a Handler
Every component with accessibilityActions needs an onAccessibilityAction handler. The repository uses a custom useOnAccessibilityAction hook pattern:
function VolumeSlider({ value, setValue }) {
let onAccessibilityAction = useOnAccessibilityAction({ updateValue: setValue, value });
return (
<View
accessible
accessibilityActions={[incrementAction, decrementAction]}
onAccessibilityAction={onAccessibilityAction}
>
<Slider />
</View>
);
}
Use accessible Prop
Mark components as accessible to ensure their accessibility props work correctly:
function MenuItem({ id, title, setMenuItem }) {
return (
<Pressable
accessible // Required for accessibility actions to work
accessibilityActions={[toggleAction, upAction, downAction]}
onAccessibilityAction={onAccessibilityAction}
>
<Text accessible={false}>{title}</Text>
</Pressable>
);
}
Disable Accessibility on Children
When a parent has accessibility props, disable accessibility on children to prevent nesting:
<Pressable accessible accessibilityActions={[...]}>
{/* Disable on children to avoid nested accessible elements */}
<Text accessible={false}>{title}</Text>
</Pressable>
Define Actions as Constants
Define action objects once at the top of your file:
// From the repository
let incrementAction = { name: "increment", label: "increment" };
let decrementAction = { name: "decrement", label: "decrement" };
// Or for list items
let toggleAction = { name: "activate", label: "Toggle checked state" };
let upAction = { name: "up", label: "Move item up" };
let downAction = { name: "down", label: "Move item down" };
Manual Validation with Screen Readers
While programmatic tests provide confidence in your implementation, final validation with VoiceOver and TalkBack ensures the experience is smooth for real users.
Important: Test with Production Builds
Always test accessibility actions with a bundled/production build, not during development with Fast Refresh enabled. The hot reloader can get out of sync when triggering actions with VoiceOver or Voice Control, leading to inconsistent behavior and false failures. Use release builds for accurate validation.
iOS VoiceOver:
- Settings > Accessibility > VoiceOver
- Quick toggle: Set your Action Button to toggle VoiceOver for instant on/off
- Alternative: Triple-click side button (if configured in Accessibility Shortcuts)
- Navigate to your component and swipe up/down to access actions
- Verify action labels are clear and actions work as expected
Android TalkBack:
- Settings > Accessibility > TalkBack
- Quick toggle: Volume key shortcut (if enabled)
- Long press on your component to access the local context menu
- Verify actions appear and function correctly
Use manual testing as a final check, but rely on automated tests for day-to-day development.
Resources
Example Repository:
- accessibility-actions - Working examples with full test coverage
Official Documentation:
Testing Tools:
Conclusion
Testing accessibility actions programmatically with React Native Testing Library gives you fast feedback and confidence in your implementation. By writing automated tests that run in CI/CD, you catch issues early and maintain accessibility as your app evolves.
Key takeaways:
- Use
fireEventwithonAccessibilityActionto test actions programmatically - Test each action independently with clear assertions
- Verify edge cases like boundary conditions and disabled states
- Use the accessibility-actions repository as a reference
- Validate manually with VoiceOver and TalkBack as a final check
Start with programmatic tests for rapid iteration, then validate with screen readers before shipping. This approach ensures accessible experiences while maintaining development velocity.