From f12e9714b7a96fee700fba4cd1978c139239abe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 13:49:28 +0200 Subject: [PATCH 1/7] feat: User Event accessibilityAction --- src/event-builder/common.ts | 16 ++++ .../__tests__/accessibility-action.test.tsx | 84 +++++++++++++++++++ .../accessibility-action.ts | 64 ++++++++++++++ src/user-event/accessibility-action/index.ts | 1 + src/user-event/index.ts | 3 + src/user-event/setup/setup.ts | 18 ++++ 6 files changed, 186 insertions(+) create mode 100644 src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx create mode 100644 src/user-event/accessibility-action/accessibility-action.ts create mode 100644 src/user-event/accessibility-action/index.ts diff --git a/src/event-builder/common.ts b/src/event-builder/common.ts index 43b6606ae..9224faa0c 100644 --- a/src/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -66,3 +66,19 @@ export function buildBlurEvent() { }, }; } + +/** + * Builds an accessibility action event, as delivered to the `onAccessibilityAction` + * handler when an assistive technology triggers an action. + * + * Experimental values: + * - `{"actionName": "increment"}` + */ +export function buildAccessibilityActionEvent(actionName: string) { + return { + ...baseSyntheticEvent(), + nativeEvent: { + actionName, + }, + }; +} diff --git a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx new file mode 100644 index 000000000..9c5551e00 --- /dev/null +++ b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import type { AccessibilityActionEvent } from 'react-native'; +import { View } from 'react-native'; + +import { render, screen, userEvent } from '../../..'; +import { createEventLogger, lastEventPayload } from '../../../test-utils/events'; + +async function renderViewWithActions(props: React.ComponentProps = {}) { + const { events, logEvent } = createEventLogger(); + + await render( + + logEvent('accessibilityAction')(event) + } + {...props} + />, + ); + + return { events }; +} + +describe('userEvent.accessibilityAction', () => { + it('triggers the onAccessibilityAction handler with the given action name', async () => { + const user = userEvent.setup(); + const { events } = await renderViewWithActions(); + + await user.accessibilityAction(screen.getByTestId('view'), 'increment'); + + expect(events).toHaveLength(1); + expect(events[0].name).toBe('accessibilityAction'); + expect(lastEventPayload(events, 'accessibilityAction').nativeEvent).toEqual({ + actionName: 'increment', + }); + }); + + it('supports the direct (setup-less) call form', async () => { + const { events } = await renderViewWithActions(); + + await userEvent.accessibilityAction(screen.getByTestId('view'), 'activate'); + + expect(events).toHaveLength(1); + expect(lastEventPayload(events, 'accessibilityAction').nativeEvent.actionName).toBe('activate'); + }); + + it('throws when the action is not declared in accessibilityActions', async () => { + const user = userEvent.setup(); + await renderViewWithActions(); + + await expect(user.accessibilityAction(screen.getByTestId('view'), 'decrement')).rejects.toThrow( + /does not declare it in the "accessibilityActions" prop.*"increment", "activate"/s, + ); + }); + + it('throws when the element declares no accessibility actions', async () => { + const user = userEvent.setup(); + await renderViewWithActions({ accessibilityActions: undefined }); + + await expect(user.accessibilityAction(screen.getByTestId('view'), 'increment')).rejects.toThrow( + /declares no accessibility actions/, + ); + }); + + it('throws when the element is disabled', async () => { + const user = userEvent.setup(); + await renderViewWithActions({ 'aria-disabled': true }); + + await expect(user.accessibilityAction(screen.getByTestId('view'), 'increment')).rejects.toThrow( + /on a disabled element/, + ); + }); + + it('throws when the element is disabled via accessibilityState', async () => { + const user = userEvent.setup(); + await renderViewWithActions({ accessibilityState: { disabled: true } }); + + await expect(user.accessibilityAction(screen.getByTestId('view'), 'increment')).rejects.toThrow( + /on a disabled element/, + ); + }); +}); diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts new file mode 100644 index 000000000..f89311f77 --- /dev/null +++ b/src/user-event/accessibility-action/accessibility-action.ts @@ -0,0 +1,64 @@ +import type { AccessibilityActionInfo } from 'react-native'; +import type { TestInstance } from 'test-renderer'; + +import { buildAccessibilityActionEvent } from '../../event-builder'; +import { computeAriaDisabled } from '../../helpers/accessibility'; +import { ErrorWithStack } from '../../helpers/errors'; +import type { StringWithAutocomplete } from '../../types'; +import type { UserEventInstance } from '../setup'; +import { dispatchEvent } from '../utils'; + +/** + * Standard accessibility action names recognized by React Native. Custom action + * names are supported as well, hence the `string` fallback. + * + * @see https://reactnative.dev/docs/accessibility#accessibility-actions + */ +export type AccessibilityActionName = StringWithAutocomplete< + 'activate' | 'increment' | 'decrement' | 'longpress' | 'magicTap' | 'escape' | 'expand' | 'collapse' +>; + +/** + * Simulate an assistive technology (e.g. screen reader) triggering an + * accessibility action on a given element. + * + * This will call the `onAccessibilityAction` handler with an event carrying the + * given `actionName`. + * + * Like a real assistive technology, the action must be declared in the element's + * `accessibilityActions` prop, and the element must not be disabled. Otherwise an + * error is thrown. + * + * @param instance element to trigger the action on + * @param actionName name of the accessibility action to trigger + */ +export async function accessibilityAction( + this: UserEventInstance, + instance: TestInstance, + actionName: AccessibilityActionName, +): Promise { + const actions = instance.props.accessibilityActions as + | ReadonlyArray + | undefined; + + if (!actions?.some((action) => action.name === actionName)) { + const declared = actions?.map((action) => action.name); + throw new ErrorWithStack( + `accessibilityAction() called with action "${actionName}", but the element does not declare it in the "accessibilityActions" prop. ${ + declared?.length + ? `Declared actions: ${declared.map((name) => `"${name}"`).join(', ')}.` + : 'The element declares no accessibility actions.' + }`, + accessibilityAction, + ); + } + + if (computeAriaDisabled(instance)) { + throw new ErrorWithStack( + `accessibilityAction() called with action "${actionName}" on a disabled element. Assistive technologies cannot trigger actions on disabled elements.`, + accessibilityAction, + ); + } + + await dispatchEvent(instance, 'accessibilityAction', buildAccessibilityActionEvent(actionName)); +} diff --git a/src/user-event/accessibility-action/index.ts b/src/user-event/accessibility-action/index.ts new file mode 100644 index 000000000..c84e7b3d9 --- /dev/null +++ b/src/user-event/accessibility-action/index.ts @@ -0,0 +1 @@ +export { accessibilityAction, AccessibilityActionName } from './accessibility-action'; diff --git a/src/user-event/index.ts b/src/user-event/index.ts index d2d0a9622..b19451cdf 100644 --- a/src/user-event/index.ts +++ b/src/user-event/index.ts @@ -1,5 +1,6 @@ import type { TestInstance } from 'test-renderer'; +import type { AccessibilityActionName } from './accessibility-action'; import type { PressOptions } from './press'; import type { ScrollToOptions } from './scroll'; import { setup } from './setup'; @@ -20,4 +21,6 @@ export const userEvent = { paste: (instance: TestInstance, text: string) => setup().paste(instance, text), scrollTo: (instance: TestInstance, options: ScrollToOptions) => setup().scrollTo(instance, options), + accessibilityAction: (instance: TestInstance, actionName: AccessibilityActionName) => + setup().accessibilityAction(instance, actionName), }; diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index 336d19f2a..feb75d32d 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -3,6 +3,8 @@ import type { TestInstance } from 'test-renderer'; import { jestFakeTimersAreEnabled } from '../../helpers/timers'; import { validateOptions } from '../../helpers/validate-options'; import { wrapAsync } from '../../helpers/wrap-async'; +import type { AccessibilityActionName } from '../accessibility-action'; +import { accessibilityAction } from '../accessibility-action'; import { clear } from '../clear'; import { paste } from '../paste'; import type { PressOptions } from '../press'; @@ -153,6 +155,21 @@ export interface UserEventInstance { * @returns */ scrollTo: (instance: TestInstance, options: ScrollToOptions) => Promise; + + /** + * Simulate an assistive technology (e.g. screen reader) triggering an + * accessibility action on a given element. + * + * The action must be declared in the element's `accessibilityActions` prop and + * the element must not be disabled, otherwise an error is thrown. + * + * @param instance element to trigger the action on + * @param actionName name of the accessibility action to trigger + */ + accessibilityAction: ( + instance: TestInstance, + actionName: AccessibilityActionName, + ) => Promise; } function createInstance(config: UserEventConfig): UserEventInstance { @@ -168,6 +185,7 @@ function createInstance(config: UserEventConfig): UserEventInstance { clear: wrapAndBindImpl(instance, clear), paste: wrapAndBindImpl(instance, paste), scrollTo: wrapAndBindImpl(instance, scrollTo), + accessibilityAction: wrapAndBindImpl(instance, accessibilityAction), }; Object.assign(instance, api); From bef8c5c564310916b2fb316be786f8e98512e974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 13:59:31 +0200 Subject: [PATCH 2/7] format --- .../accessibility-action/accessibility-action.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts index f89311f77..641270e75 100644 --- a/src/user-event/accessibility-action/accessibility-action.ts +++ b/src/user-event/accessibility-action/accessibility-action.ts @@ -15,7 +15,14 @@ import { dispatchEvent } from '../utils'; * @see https://reactnative.dev/docs/accessibility#accessibility-actions */ export type AccessibilityActionName = StringWithAutocomplete< - 'activate' | 'increment' | 'decrement' | 'longpress' | 'magicTap' | 'escape' | 'expand' | 'collapse' + | 'activate' + | 'increment' + | 'decrement' + | 'longpress' + | 'magicTap' + | 'escape' + | 'expand' + | 'collapse' >; /** From 4b40f8193b4993536aec408503317e71512ba933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 14:02:11 +0200 Subject: [PATCH 3/7] docs --- docs/api/user-event.md | 29 +++++++++++++++++++ .../accessibility-action.ts | 19 +++++------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/api/user-event.md b/docs/api/user-event.md index 04e4375c7..b7e934639 100644 --- a/docs/api/user-event.md +++ b/docs/api/user-event.md @@ -293,3 +293,32 @@ The sequence of events depends on whether the scroll includes an optional moment - `momentumScrollBegin` - `scroll` (multiple events) - `momentumScrollEnd` + +## `accessibilityAction()` + +```ts +accessibilityAction( + instance: TestInstance, + actionName: string, +): Promise +``` + +Example + +```ts +const user = userEvent.setup(); +await user.accessibilityAction(slider, 'increment'); +``` + +Simulates an assistive technology (e.g. a screen reader) triggering the named accessibility action on a given element, invoking its `onAccessibilityAction` handler. + +The `actionName` autocompletes the [standard React Native actions](https://reactnative.dev/docs/accessibility#accessibility-actions) (`activate`, `increment`, `decrement`, `longpress`, `magicTap`, `escape`), but any custom action name is accepted. + +Just like a real assistive technology, the action must be reachable by the user, otherwise an error is thrown: + +- the action must be declared in the element's `accessibilityActions` prop, +- the element must not be disabled (via `aria-disabled` or `accessibilityState={{ disabled: true }}`). + +### Sequence of events + +- `accessibilityAction` diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts index 641270e75..07bf954df 100644 --- a/src/user-event/accessibility-action/accessibility-action.ts +++ b/src/user-event/accessibility-action/accessibility-action.ts @@ -1,4 +1,7 @@ -import type { AccessibilityActionInfo } from 'react-native'; +import type { + AccessibilityActionInfo, + AccessibilityActionName as ReactNativeAccessibilityActionName, +} from 'react-native'; import type { TestInstance } from 'test-renderer'; import { buildAccessibilityActionEvent } from '../../event-builder'; @@ -9,21 +12,13 @@ import type { UserEventInstance } from '../setup'; import { dispatchEvent } from '../utils'; /** - * Standard accessibility action names recognized by React Native. Custom action + * Standard accessibility action names recognized by React Native (`activate`, + * `increment`, `decrement`, `longpress`, `magicTap`, `escape`). Custom action * names are supported as well, hence the `string` fallback. * * @see https://reactnative.dev/docs/accessibility#accessibility-actions */ -export type AccessibilityActionName = StringWithAutocomplete< - | 'activate' - | 'increment' - | 'decrement' - | 'longpress' - | 'magicTap' - | 'escape' - | 'expand' - | 'collapse' ->; +export type AccessibilityActionName = StringWithAutocomplete; /** * Simulate an assistive technology (e.g. screen reader) triggering an From b5869812dbf54b2668e522d1d0c879c1992ec4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 14:34:41 +0200 Subject: [PATCH 4/7] code review changes --- .../__tests__/accessibility-action.test.tsx | 12 ++++----- .../accessibility-action.ts | 25 +++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx index 9c5551e00..2bf68e2c4 100644 --- a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx +++ b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx @@ -24,7 +24,7 @@ async function renderViewWithActions(props: React.ComponentProps = } describe('userEvent.accessibilityAction', () => { - it('triggers the onAccessibilityAction handler with the given action name', async () => { + test('triggers the onAccessibilityAction handler with the given action name', async () => { const user = userEvent.setup(); const { events } = await renderViewWithActions(); @@ -37,7 +37,7 @@ describe('userEvent.accessibilityAction', () => { }); }); - it('supports the direct (setup-less) call form', async () => { + test('supports the direct (setup-less) call form', async () => { const { events } = await renderViewWithActions(); await userEvent.accessibilityAction(screen.getByTestId('view'), 'activate'); @@ -46,7 +46,7 @@ describe('userEvent.accessibilityAction', () => { expect(lastEventPayload(events, 'accessibilityAction').nativeEvent.actionName).toBe('activate'); }); - it('throws when the action is not declared in accessibilityActions', async () => { + test('throws when the action is not declared in accessibilityActions', async () => { const user = userEvent.setup(); await renderViewWithActions(); @@ -55,7 +55,7 @@ describe('userEvent.accessibilityAction', () => { ); }); - it('throws when the element declares no accessibility actions', async () => { + test('throws when the element declares no accessibility actions', async () => { const user = userEvent.setup(); await renderViewWithActions({ accessibilityActions: undefined }); @@ -64,7 +64,7 @@ describe('userEvent.accessibilityAction', () => { ); }); - it('throws when the element is disabled', async () => { + test('throws when the element is disabled', async () => { const user = userEvent.setup(); await renderViewWithActions({ 'aria-disabled': true }); @@ -73,7 +73,7 @@ describe('userEvent.accessibilityAction', () => { ); }); - it('throws when the element is disabled via accessibilityState', async () => { + test('throws when the element is disabled via accessibilityState', async () => { const user = userEvent.setup(); await renderViewWithActions({ accessibilityState: { disabled: true } }); diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts index 07bf954df..ae5d902dd 100644 --- a/src/user-event/accessibility-action/accessibility-action.ts +++ b/src/user-event/accessibility-action/accessibility-action.ts @@ -6,6 +6,7 @@ import type { TestInstance } from 'test-renderer'; import { buildAccessibilityActionEvent } from '../../event-builder'; import { computeAriaDisabled } from '../../helpers/accessibility'; +import { isTestInstance } from '../../helpers/component-tree'; import { ErrorWithStack } from '../../helpers/errors'; import type { StringWithAutocomplete } from '../../types'; import type { UserEventInstance } from '../setup'; @@ -39,18 +40,28 @@ export async function accessibilityAction( instance: TestInstance, actionName: AccessibilityActionName, ): Promise { + if (!isTestInstance(instance)) { + throw new ErrorWithStack( + `accessibilityAction() works only with host instances.`, + accessibilityAction, + ); + } + const actions = instance.props.accessibilityActions as | ReadonlyArray | undefined; - if (!actions?.some((action) => action.name === actionName)) { - const declared = actions?.map((action) => action.name); + if (!actions?.length) { + throw new ErrorWithStack( + `accessibilityAction() called with action "${actionName}", but the element declares no accessibility actions in the "accessibilityActions" prop.`, + accessibilityAction, + ); + } + + if (!actions.some((action) => action.name === actionName)) { + const declared = actions.map((action) => `"${action.name}"`).join(', '); throw new ErrorWithStack( - `accessibilityAction() called with action "${actionName}", but the element does not declare it in the "accessibilityActions" prop. ${ - declared?.length - ? `Declared actions: ${declared.map((name) => `"${name}"`).join(', ')}.` - : 'The element declares no accessibility actions.' - }`, + `accessibilityAction() called with action "${actionName}", but the element does not declare it in the "accessibilityActions" prop. Declared actions: ${declared}.`, accessibilityAction, ); } From 10dfa2611e27858bde9c5eef6694d0a59d4ee09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 14:38:31 +0200 Subject: [PATCH 5/7] msg tweaks --- .../__tests__/accessibility-action.test.tsx | 4 ++-- .../accessibility-action/accessibility-action.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx index 2bf68e2c4..87f349158 100644 --- a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx +++ b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx @@ -51,7 +51,7 @@ describe('userEvent.accessibilityAction', () => { await renderViewWithActions(); await expect(user.accessibilityAction(screen.getByTestId('view'), 'decrement')).rejects.toThrow( - /does not declare it in the "accessibilityActions" prop.*"increment", "activate"/s, + /has no "decrement" accessibility action.*"increment", "activate"/s, ); }); @@ -60,7 +60,7 @@ describe('userEvent.accessibilityAction', () => { await renderViewWithActions({ accessibilityActions: undefined }); await expect(user.accessibilityAction(screen.getByTestId('view'), 'increment')).rejects.toThrow( - /declares no accessibility actions/, + /has no accessibility actions/, ); }); diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts index ae5d902dd..7b61bb8b4 100644 --- a/src/user-event/accessibility-action/accessibility-action.ts +++ b/src/user-event/accessibility-action/accessibility-action.ts @@ -53,22 +53,22 @@ export async function accessibilityAction( if (!actions?.length) { throw new ErrorWithStack( - `accessibilityAction() called with action "${actionName}", but the element declares no accessibility actions in the "accessibilityActions" prop.`, + `The element has no accessibility actions. Add them using the "accessibilityActions" prop.`, accessibilityAction, ); } if (!actions.some((action) => action.name === actionName)) { - const declared = actions.map((action) => `"${action.name}"`).join(', '); + const available = actions.map((action) => `"${action.name}"`).join(', '); throw new ErrorWithStack( - `accessibilityAction() called with action "${actionName}", but the element does not declare it in the "accessibilityActions" prop. Declared actions: ${declared}.`, + `The element has no "${actionName}" accessibility action. Available actions: ${available}.`, accessibilityAction, ); } if (computeAriaDisabled(instance)) { throw new ErrorWithStack( - `accessibilityAction() called with action "${actionName}" on a disabled element. Assistive technologies cannot trigger actions on disabled elements.`, + `Cannot trigger the "${actionName}" accessibility action on a disabled element.`, accessibilityAction, ); } From 8362699c69c46594df04cac26eb75adaa2dc4acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 14:47:30 +0200 Subject: [PATCH 6/7] tweaks --- docs/api/user-event.md | 2 +- .../accessibility-action.ts | 21 +++++++++----- .../docs/14.x/docs/api/events/user-event.mdx | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/api/user-event.md b/docs/api/user-event.md index b7e934639..7991ec3f3 100644 --- a/docs/api/user-event.md +++ b/docs/api/user-event.md @@ -312,7 +312,7 @@ await user.accessibilityAction(slider, 'increment'); Simulates an assistive technology (e.g. a screen reader) triggering the named accessibility action on a given element, invoking its `onAccessibilityAction` handler. -The `actionName` autocompletes the [standard React Native actions](https://reactnative.dev/docs/accessibility#accessibility-actions) (`activate`, `increment`, `decrement`, `longpress`, `magicTap`, `escape`), but any custom action name is accepted. +The `actionName` autocompletes the [standard React Native actions](https://reactnative.dev/docs/accessibility#accessibility-actions) (`activate`, `increment`, `decrement`, `longpress`, `magicTap`, `escape`, `expand`, `collapse`), but any custom action name is accepted. Just like a real assistive technology, the action must be reachable by the user, otherwise an error is thrown: diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts index 7b61bb8b4..bfa40c6fd 100644 --- a/src/user-event/accessibility-action/accessibility-action.ts +++ b/src/user-event/accessibility-action/accessibility-action.ts @@ -1,7 +1,4 @@ -import type { - AccessibilityActionInfo, - AccessibilityActionName as ReactNativeAccessibilityActionName, -} from 'react-native'; +import type { AccessibilityActionInfo } from 'react-native'; import type { TestInstance } from 'test-renderer'; import { buildAccessibilityActionEvent } from '../../event-builder'; @@ -14,12 +11,22 @@ import { dispatchEvent } from '../utils'; /** * Standard accessibility action names recognized by React Native (`activate`, - * `increment`, `decrement`, `longpress`, `magicTap`, `escape`). Custom action - * names are supported as well, hence the `string` fallback. + * `increment`, `decrement`, `longpress`, `magicTap`, `escape`, `expand`, + * `collapse`). Custom action names are supported as well, hence the `string` + * fallback. * * @see https://reactnative.dev/docs/accessibility#accessibility-actions */ -export type AccessibilityActionName = StringWithAutocomplete; +export type AccessibilityActionName = StringWithAutocomplete< + | 'activate' + | 'increment' + | 'decrement' + | 'longpress' + | 'magicTap' + | 'escape' + | 'expand' + | 'collapse' +>; /** * Simulate an assistive technology (e.g. screen reader) triggering an diff --git a/website/docs/14.x/docs/api/events/user-event.mdx b/website/docs/14.x/docs/api/events/user-event.mdx index 198dc0c13..ce9b153b7 100644 --- a/website/docs/14.x/docs/api/events/user-event.mdx +++ b/website/docs/14.x/docs/api/events/user-event.mdx @@ -294,3 +294,32 @@ The sequence of events depends on whether the scroll includes an optional moment - `momentumScrollBegin` - `scroll` (multiple events) - `momentumScrollEnd` + +## `accessibilityAction()` + +```ts +accessibilityAction( + instance: TestInstance, + actionName: string, +): Promise +``` + +Example + +```ts +const user = userEvent.setup(); +await user.accessibilityAction(slider, 'increment'); +``` + +Simulates an assistive technology (e.g. a screen reader) triggering the named accessibility action on a given element, invoking its `onAccessibilityAction` handler. + +The `actionName` autocompletes the [standard React Native actions](https://reactnative.dev/docs/accessibility#accessibility-actions) (`activate`, `increment`, `decrement`, `longpress`, `magicTap`, `escape`, `expand`, `collapse`), but any custom action name is accepted. + +Just like a real assistive technology, the action must be reachable by the user, otherwise an error is thrown: + +- the action must be declared in the element's `accessibilityActions` prop, +- the element must not be disabled (via `aria-disabled` or `accessibilityState={{ disabled: true }}`). + +### Sequence of events + +- `accessibilityAction` From 47f0f55e5b8682da856f1cab2d4fbc4cf9ab3866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 14:57:46 +0200 Subject: [PATCH 7/7] cov --- .../__tests__/accessibility-action.test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx index 87f349158..8421504d6 100644 --- a/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx +++ b/src/user-event/accessibility-action/__tests__/accessibility-action.test.tsx @@ -46,6 +46,16 @@ describe('userEvent.accessibilityAction', () => { expect(lastEventPayload(events, 'accessibilityAction').nativeEvent.actionName).toBe('activate'); }); + test('throws when passed a non-host instance', async () => { + const user = userEvent.setup(); + await renderViewWithActions(); + + // @ts-expect-error intentionally passing a non-host instance + await expect(user.accessibilityAction('not a host instance', 'increment')).rejects.toThrow( + /works only with host instances/, + ); + }); + test('throws when the action is not declared in accessibilityActions', async () => { const user = userEvent.setup(); await renderViewWithActions();