How do I test Accessibility Actions in React Native?

August 7, 2024

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:

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 fireEvent with onAccessibilityAction to 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.