Repository: wix/Detox (js)

Issue:
Add support for accessibilityActions
### Describe your idea

(Opened in retrospect for #3877)
Introduce an element-action API which would invoke its potential [`accessibilityActions`](https://reactnative.dev/docs/accessibility#accessibility-actions) specified action.

Example:

```js
// Component
<TouchableOpacity testID="button" accessibilityActions={[{ name: 'activate', label: 'activate' }]}>
  // ...
</TouchableOpacity>

// Test
await element(by.id('button')).performAccessibilityAction('activate');
```


Pull request:
feat: element(...).performAccessibilityAction() API
## Description

<!--
Thank you for contributing!

Step 1: Before submitting a pull request that introduces a new functionality or fixes a bug,
please open an issue where we could discuss the suggestion, problem - and potential ways to fix.
-->

<!--
Step 2: Provide an overview of how your fix / enhancement works.
If possible, provide screenshots of the before and after states (even for simple command line options - show the terminal).
-->

In this pull request, I have added a new action named `performAccessibilityAction` which enables the triggering of `accessibilityActions` within tests. Resolves #3882.

For example

```js
// Component
<TouchableOpacity 
  testID="View7991"
  accessibilityActions={[{ name: 'activate', label: 'activate' }]}
>
...
</TouchableOpacity>

// Test
await element(by.id("View7991")).performAccessibilityAction('activate');
```

This feature was mostly copied from https://github.com/wix/Detox/pull/2353 which seems to have been abandoned and closed back in 2020. I have used those changes and updated them to work with the current Detox.

I went ahead and made the PR without creating an issue first as people seemed to be in favor of this new feature in the original PR. Please let me know if you'd like me to open an issue for it first instead.

<!--
Step 3: Please review the checklist below.
-->

---


> _For features/enhancements:_
 - [x] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.

> _For API changes:_
 - [x] I have made the necessary changes in the [types index](https://github.com/wix/Detox/blob/master/detox/index.d.ts) file.


Patch:
diff --git a/.editorconfig b/.editorconfig
index 702836fc4..035c41a1d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -26,5 +26,6 @@ tab_width = 2
 indent_size = 2
 tab_width = 2
 
-[{*.css, *.scss, *.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
+[{*.css,*.scss,*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
 indent_size = 2
+tab_width = 2
diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
index fbf9b704e..8474e3526 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
+++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
@@ -5,6 +5,7 @@ import android.os.Build;
 
 import com.wix.detox.common.DetoxErrors.DetoxRuntimeException;
 import com.wix.detox.common.DetoxErrors.StaleActionException;
+import com.wix.detox.espresso.action.RNDetoxAccessibilityAction;
 import com.wix.detox.espresso.action.AdjustSliderToPositionAction;
 import com.wix.detox.espresso.action.DetoxMultiTap;
 import com.wix.detox.espresso.action.RNClickAction;
@@ -203,6 +204,10 @@ public class DetoxAction {
         };
     }
 
+    public static ViewAction accessibilityAction(final String actionName) {
+        return new RNDetoxAccessibilityAction(actionName);
+    }
+
     private static Date parseDateISO8601(String dateString) throws ParseException {
         try {
             return new SimpleDateFormat(ISO8601_FORMAT).parse(dateString);
diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNDetoxAccessibilityAction.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNDetoxAccessibilityAction.kt
new file mode 100644
index 000000000..aa50a7385
--- /dev/null
+++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNDetoxAccessibilityAction.kt
@@ -0,0 +1,46 @@
+package com.wix.detox.espresso.action
+
+import android.view.View
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.ReactContext
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.uimanager.UIManagerHelper
+import com.facebook.react.uimanager.events.Event
+import com.wix.detox.espresso.DetoxMatcher
+import org.hamcrest.Matcher
+
+class RNDetoxAccessibilityAction(private val mActionName: String) : ViewAction {
+
+    override fun getConstraints(): Matcher<View?>? = DetoxMatcher.matcherForNotNull()
+
+    override fun getDescription(): String = "Dispatch an Accessibility Action"
+
+    override fun perform(uiController: UiController?, view: View?) {
+        val reactContext = view?.context as? ReactContext ?: return
+        val reactTag = view.id
+
+        UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag)
+            ?.dispatchEvent(
+                TopAccessibilityEvent(
+                    surfaceId = UIManagerHelper.getSurfaceId(reactContext),
+                    viewId = reactTag,
+                    actionName = mActionName,
+                )
+            )
+
+        val waitTimeMS = 100
+        uiController!!.loopMainThreadForAtLeast(waitTimeMS.toLong())
+    }
+}
+
+class TopAccessibilityEvent(surfaceId: Int, viewId: Int, private val actionName: String) :
+    Event<TopAccessibilityEvent>(surfaceId, viewId) {
+
+    override fun getEventName(): String = "topAccessibilityAction"
+
+    override fun getEventData(): WritableMap? {
+        return Arguments.createMap().apply { putString("actionName", actionName) }
+    }
+}
diff --git a/detox/index.d.ts b/detox/index.d.ts
index be741cf34..8c4e85f68 100644
--- a/detox/index.d.ts
+++ b/detox/index.d.ts
@@ -1380,6 +1380,13 @@ declare global {
              */
             setDatePickerDate(dateString: string, dateFormat: string): Promise<void>;
 
+            /**
+             * Triggers a given [accessibility action]{@link https://reactnative.dev/docs/accessibility#accessibility-actions}.
+             * @param actionName - name of the accessibility action
+             * @example await element(by.id('view')).performAccessibilityAction('activate');
+             */
+            performAccessibilityAction(actionName: string): Promise<void>
+
             /**
              * Pinches in the given direction with speed and angle. (iOS only)
              * @param angle value in radiant, default is `0`
diff --git a/detox/ios/Detox/Invocation/Action.swift b/detox/ios/Detox/Invocation/Action.swift
index 64e630d7c..a4ac09db3 100644
--- a/detox/ios/Detox/Invocation/Action.swift
+++ b/detox/ios/Detox/Invocation/Action.swift
@@ -33,6 +33,7 @@ class Action : CustomStringConvertible {
 		static let tap = "tap"
 		static let longPress = "longPress"
 		static let multiTap = "multiTap"
+		static let accessibilityAction = "accessibilityAction"
 		
 		static let tapBackspaceKey = "tapBackspaceKey"
 		static let tapReturnKey = "tapReturnKey"
@@ -69,6 +70,7 @@ class Action : CustomStringConvertible {
 		Kind.tap: TapAction.self,
 		Kind.longPress: LongPressAction.self,
 		Kind.multiTap: MultiTapAction.self,
+		Kind.accessibilityAction: AccessibilityActionAction.self,
 		
 		Kind.tapBackspaceKey: TypeTextAction.self,
 		Kind.tapReturnKey: TypeTextAction.self,
@@ -271,6 +273,16 @@ class MultiTapAction : Action {
 	}
 }
 
+class AccessibilityActionAction : Action {
+	override func perform(on element: Element) -> [String: Any]? {
+		let actionName = params!.first as! String
+
+		element.performAccessibilityAction(actionName)
+
+		return nil
+	}
+}
+
 class TypeTextAction : Action {
 	override func perform(on element: Element) -> [String: Any]? {
 		let text = params!.first as! String
diff --git a/detox/ios/Detox/Invocation/Element.swift b/detox/ios/Detox/Invocation/Element.swift
index 2150b3cfc..5570ab5ba 100644
--- a/detox/ios/Detox/Invocation/Element.swift
+++ b/detox/ios/Detox/Invocation/Element.swift
@@ -167,6 +167,14 @@ class Element : NSObject {
 	func replaceText(_ text: String) {
 		view.dtx_replaceText(text)
 	}
+
+	func performAccessibilityAction(_ actionName: String) {
+		guard let action = view.accessibilityCustomActions?.first(where: { $0.name == actionName }) else {
+			dtx_fatalError("Accessibility custom action with name “\(actionName)” not found for view “\(view.dtx_shortDescription)”", viewDescription: debugAttributes)
+		}
+
+		action.target?.performSelector(onMainThread: action.selector, with: action, waitUntilDone: true)
+	}
 	
 	func adjust(toDate date: Date) {
 		if let view = view as? UIDatePicker {
diff --git a/detox/ios/DetoxSync b/detox/ios/DetoxSync
index cc74f7b95..5a812ba0e 160000
--- a/detox/ios/DetoxSync
+++ b/detox/ios/DetoxSync
@@ -1,1 +1,1 @@
-Subproject commit cc74f7b95e7b07e2f384e3e2a2c373f8ab766d10
+Subproject commit 5a812ba0eb2e05e14703fa02481f0c88c2b34b7c
diff --git a/detox/package.json b/detox/package.json
index 74b1ef10c..cc2a5200b 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "bin": {
     "detox": "local-cli/cli.js"
   },
@@ -64,7 +64,7 @@
     "chalk": "^4.0.0",
     "child-process-promise": "^2.2.0",
     "execa": "^5.1.1",
-    "find-up": "^4.1.0",
+    "find-up": "^5.0.0",
     "fs-extra": "^11.0.0",
     "funpermaproxy": "^1.1.0",
     "glob": "^8.0.3",
@@ -88,8 +88,8 @@
     "trace-event-lib": "^1.3.1",
     "which": "^1.3.1",
     "ws": "^7.0.0",
-    "yargs": "^16.0.3",
-    "yargs-parser": "^20.2.9",
+    "yargs": "^17.0.0",
+    "yargs-parser": "^21.0.0",
     "yargs-unparser": "^2.0.0"
   },
   "peerDependencies": {
diff --git a/detox/src/android/actions/native.js b/detox/src/android/actions/native.js
index 25de514e5..601bef562 100644
--- a/detox/src/android/actions/native.js
+++ b/detox/src/android/actions/native.js
@@ -145,6 +145,13 @@ class TakeElementScreenshot extends Action {
   }
 }
 
+class AccessibilityActionAction extends Action {
+  constructor(actionName) {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.accessibilityAction(actionName));
+  }
+}
+
 module.exports = {
   Action,
   TapAction,
@@ -164,4 +171,5 @@ module.exports = {
   ScrollToIndex,
   SetDatePickerDateAction,
   AdjustSliderToPosition,
+  AccessibilityActionAction
 };
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index 404946451..4e7a1969d 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -169,6 +169,11 @@ class NativeElement {
     const traceDescription = actionDescription.adjustSliderToPosition(newPosition);
     return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
+
+  async performAccessibilityAction(actionName) {
+    const traceDescription = actionDescription.performAccessibilityAction(actionName);
+    return await new ActionInteraction(this._invocationManager, this, new actions.AccessibilityActionAction(actionName), traceDescription).execute();
+  }
 }
 
 module.exports = {
diff --git a/detox/src/android/espressoapi/DetoxAction.js b/detox/src/android/espressoapi/DetoxAction.js
index 3628b193b..6ed2d213f 100644
--- a/detox/src/android/espressoapi/DetoxAction.js
+++ b/detox/src/android/espressoapi/DetoxAction.js
@@ -233,6 +233,18 @@ class DetoxAction {
     };
   }
 
+  static accessibilityAction(actionName) {
+    if (typeof actionName !== "string") throw new Error("actionName should be a string, but got " + (actionName + (" (" + (typeof actionName + ")"))));
+    return {
+      target: {
+        type: "Class",
+        value: "com.wix.detox.espresso.DetoxAction"
+      },
+      method: "accessibilityAction",
+      args: [actionName]
+    };
+  }
+
   static parseDateISO8601(dateString) {
     if (typeof dateString !== "string") throw new Error("dateString should be a string, but got " + (dateString + (" (" + (typeof dateString + ")"))));
     return {
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index 3415b420b..45f8eacfd 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -229,6 +229,13 @@ class Element {
     return this.withAction('clearText', traceDescription);
   }
 
+  performAccessibilityAction(actionName) {
+    if (typeof actionName !== 'string') throw new Error('actionName should be a string, but got ' + (actionName + (' (' + (typeof actionName + ')'))));
+    
+    const traceDescription = actionDescription.performAccessibilityAction(actionName);
+    return this.withAction('accessibilityAction', traceDescription, actionName);
+  }
+
   scroll(pixels, direction = 'down', startPositionX = NaN, startPositionY = NaN) {
     if (!['left', 'right', 'up', 'down'].some(option => option === direction)) throw new Error('direction should be one of [left, right, up, down], but got ' + direction);
     if (typeof pixels !== 'number') throw new Error('amount of pixels should be a number, but got ' + (pixels + (' (' + (typeof pixels + ')'))));
@@ -660,6 +667,12 @@ class WaitFor {
     return this.waitForWithAction(traceDescription);
   }
 
+  performAccessibilityAction(actionName) {
+    this.action = this.actionableElement.performAccessibilityAction(actionName);
+    const traceDescription = actionDescription.performAccessibilityAction(actionName);
+    return this.waitForWithAction(traceDescription);
+  }
+
   pinch(scale, speed, angle) {
     this.action = this.actionableElement.pinch(scale, speed, angle);
     const traceDescription = actionDescription.pinch(scale, speed, angle);
diff --git a/detox/src/utils/invocationTraceDescriptions.js b/detox/src/utils/invocationTraceDescriptions.js
index 246699623..a9168327a 100644
--- a/detox/src/utils/invocationTraceDescriptions.js
+++ b/detox/src/utils/invocationTraceDescriptions.js
@@ -7,6 +7,7 @@ module.exports = {
     longPressAndDrag: (duration, startX, startY, targetElement, endX, endY, speed, holdDuration) =>
       `long press and drag from ${startX}, ${startY} to ${endX}, ${endY} with speed ${speed} and hold duration ${holdDuration}`,
     multiTap: (times) => `tap ${times} times`,
+    performAccessibilityAction: (actionName) => `perform ${actionName} accessibilityAction`,
     pinch: (scale, speed, angle) => `pinch with scale ${scale}, speed ${speed}, and angle ${angle}`,
     pinchWithAngle: (direction, speed, angle) => `pinch with direction ${direction}, speed ${speed}, and angle ${angle}`,
     replaceText: (value) => `replace input text: "${value}"`,
diff --git a/docs/api/actions.md b/docs/api/actions.md
index 10e50298f..75cb77a04 100644
--- a/docs/api/actions.md
+++ b/docs/api/actions.md
@@ -26,6 +26,7 @@ Use [expectations](expect.md) to verify element states.
 - [`.adjustSliderToPosition()`](#adjustslidertopositionnormalizedposition)
 - [`.getAttributes()`](#getattributes)
 - [`.takeScreenshot(name)`](#takescreenshotname)
+- [`.performAccessibilityAction()`](#performaccessibilityactionactionname)
 
 ### `tap(point)`
 
@@ -330,6 +331,16 @@ Takes a screenshot of the matched element. For full details on taking screenshot
 
 `name`—the name of the screenshot
 
+### `performAccessibilityAction(actionName)`
+
+Triggers an [accessibility action](https://reactnative.dev/docs/accessibility#accessibility-actions).
+
+`actionName`—the name of the accessibility action <br/>
+
+```js
+await element(by.id('scrollView')).performAccessibilityAction("activate");
+```
+
 ## Deprecated Methods
 
 - [`.tapAtPoint()`](#tapatpointpoint)
diff --git a/docs/demo.mdx b/docs/demo.mdx
index f9ea82c9b..a857e1a4c 100644
--- a/docs/demo.mdx
+++ b/docs/demo.mdx
@@ -49,6 +49,10 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismo
 
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
 
+### `example.​heading.​with.​a.​very.​very.​and.​really.​very.​long.​line.​which.​should.​be.​wrapped` \[string \| number\]
+
+- Lorem ipsum dolor sit amet, consectetur elit, sed do eiusmod tempor `incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam`, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.)
+
 ###### `id` \[string \| number\]
 
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index dc3e7288a..01bcfc1e8 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,13 +1,13 @@
 {
   "name": "detox-demo-native-android",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "private": true,
   "scripts": {
     "packager": "react-native start",
     "detox-server": "detox run-server"
   },
   "devDependencies": {
-    "detox": "^20.7.0"
+    "detox": "^20.7.2"
   },
   "detox": {}
 }
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index ae949c804..8b1f96bac 100644
--- a/examples/demo-native-ios/package.json
+++ b/examples/demo-native-ios/package.json
@@ -1,9 +1,9 @@
 {
   "name": "detox-demo-native-ios",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "private": true,
   "devDependencies": {
-    "detox": "^20.7.0"
+    "detox": "^20.7.2"
   },
   "detox": {
     "specs": "",
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index f8c80f4ad..99ef9d054 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^20.7.0",
+    "detox": "^20.7.2",
     "jest": "^28.0.0"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index f10dc6f8f..e7acd4c2c 100644
--- a/examples/demo-react-native-detox-instruments/package.json
+++ b/examples/demo-react-native-detox-instruments/package.json
@@ -1,10 +1,10 @@
 {
   "name": "demo-react-native-detox-instruments",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^20.7.0"
+    "detox": "^20.7.2"
   },
   "detox": {
     "configurations": {
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 193ad2e5d..455f1a27b 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -30,7 +30,7 @@
   "devDependencies": {
     "@types/fs-extra": "^9.0.13",
     "@types/jest": "^29.2.1",
-    "detox": "^20.7.0",
+    "detox": "^20.7.2",
     "fs-extra": "^9.1.0",
     "jest": "^29.2.1",
     "ts-jest": "^29.0.3",
diff --git a/lerna.json b/lerna.json
index 733f8f923..58a86161e 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "20.7.0",
+  "version": "20.7.2",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index 33c1e59bf..0246ee189 100644
--- a/package.json
+++ b/package.json
@@ -54,5 +54,5 @@
     "shell-utils": "1.x.x",
     "unified": "^10.1.0"
   },
-  "version": "20.7.0"
+  "version": "20.7.2"
 }
diff --git a/website/package.json b/website/package.json
index 8116d8af5..e8da506fc 100644
--- a/website/package.json
+++ b/website/package.json
@@ -23,6 +23,7 @@
     "@percy/cli": "^1.17.0",
     "@svgr/webpack": "^5.5.0",
     "clsx": "^1.1.1",
+    "docusaurus": "^1.14.7",
     "docusaurus-plugin-sass": "^0.2.3",
     "file-loader": "^6.2.0",
     "prism-react-renderer": "^1.2.1",
diff --git a/website/percy.mjs b/website/percy.mjs
index f979faa00..21a24782c 100644
--- a/website/percy.mjs
+++ b/website/percy.mjs
@@ -39,7 +39,7 @@ class Server {
       this.childPromise.stdout.on('data', (data) => {
         if (`${data}`.startsWith('[SUCCESS] Serving')) {
           clearTimeout(handle);
-          resolve();
+          setTimeout(resolve, 2000);
         }
       });
     });
diff --git a/website/src/css/_markdown.scss b/website/src/css/_markdown.scss
index 02667550d..6114f29e2 100644
--- a/website/src/css/_markdown.scss
+++ b/website/src/css/_markdown.scss
@@ -4,6 +4,16 @@
   padding: 0 var(--ifm-pre-padding);
 }
 
-.markdown code {
-  white-space: pre;
+.markdown {
+  @media (min-width: 768px) {
+    code {
+      white-space: pre;
+    }
+  }
+
+  h1, h2, h3, h4, h5, h6 {
+    code {
+      white-space: pre-wrap;
+    }
+  }
 }
diff --git a/website/versioned_docs/version-20.x/demo.mdx b/website/versioned_docs/version-20.x/demo.mdx
index f9ea82c9b..a857e1a4c 100644
--- a/website/versioned_docs/version-20.x/demo.mdx
+++ b/website/versioned_docs/version-20.x/demo.mdx
@@ -49,6 +49,10 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismo
 
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
 
+### `example.​heading.​with.​a.​very.​very.​and.​really.​very.​long.​line.​which.​should.​be.​wrapped` \[string \| number\]
+
+- Lorem ipsum dolor sit amet, consectetur elit, sed do eiusmod tempor `incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam`, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.)
+
 ###### `id` \[string \| number\]
 
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.


Test patch:
diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
index 01dbe95df..955f3c01e 100644
--- a/detox/src/android/AndroidExpect.test.js
+++ b/detox/src/android/AndroidExpect.test.js
@@ -304,6 +304,10 @@ describe('AndroidExpect', () => {
       it('should adjust slider to position', async () => {
         await e.element(e.by.id('sliderWithASimpleID')).adjustSliderToPosition(75);
       });
+
+      it('should perform accessibility actions', async () => {
+        await e.element(e.by.id('View7991')).performAccessibilityAction('activate');
+      });
     });
 
     describe('element screenshots', () => {
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index 2710d7b4a..5ae66a261 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -622,6 +622,25 @@ describe('expectTwo', () => {
   it('web.element() should throw', async () => {
     expect(() => e.web.element(e.by.id('someId'))).toThrowError(/not support/);
   });
+
+  it(`element(e.by.text('tapMe')).performAccessibilityAction('activate')`, async () => {
+    const testCall = await e.element(e.by.text('tapMe')).performAccessibilityAction('activate');
+    const jsonOutput = {
+      invocation: {
+        type: 'action',
+        action: 'accessibilityAction',
+        predicate: {
+          type: 'text',
+          value: 'tapMe'
+        },
+        params: [
+          'activate'
+        ]
+      }
+    };
+
+    expect(testCall).toDeepEqual(jsonOutput);
+  });
 });
 
 expect.extend({
diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js
index f70e6a568..53ab4dd6b 100644
--- a/detox/src/ios/expectTwoApiCoverage.test.js
+++ b/detox/src/ios/expectTwoApiCoverage.test.js
@@ -150,6 +150,7 @@ describe('expectTwo API Coverage', () => {
       await e.element(e.by.id('someId')).atIndex(1).tap();
       await e.element(e.by.id('someId')).setDatePickerDate('2019-2-8T05:10:00-08:00', 'yyyy-MM-dd\'T\'HH:mm:ssZZZZZ');
       await e.element(e.by.id('slider')).adjustSliderToPosition(0.5);
+      await e.element(e.by.id('someId')).performAccessibilityAction('activate');
     });
 
     it(`interactions with wrong parameters should throw`, async () => {
@@ -203,6 +204,8 @@ describe('expectTwo API Coverage', () => {
       await expectToThrow(() => e.element(e.by.id('elementToDrag')).longPressAndDrag(1000, 0.5, 0.5, e.by.id('matcherNotElement')));
       await expectToThrow(() => e.element(e.by.id('elementToDrag')).longPressAndDrag('notANumber', 0.5, 0.5, e.element(e.by.id('targetElement'))));
       await expectToThrow(() => e.element(e.by.id('elementToDrag')).longPressAndDrag(1000, 0.5, 0.5, e.element(e.by.id('targetElement')), 0.5, 0.5, 'slow', 'notANumber'));
+
+      await expectToThrow(() => e.element(e.by.id('someId')).performAccessibilityAction());
     });
 
   });
@@ -244,6 +247,7 @@ describe('expectTwo API Coverage', () => {
       // await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).clearText());
       await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'notADirection', 0, 0));
       await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('notADirection'));
+      await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).performAccessibilityAction());
     });
 
     it(`waitFor....whileElement() actions`, async () => {
@@ -269,6 +273,7 @@ describe('expectTwo API Coverage', () => {
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).setDatePickerDate('2019-2-8T05:10:00-08:00', 'yyyy-MM-dd\'T\'HH:mm:ssZZZZZ');
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).pinchWithAngle('outward', 'fast', 0);
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).pinch(1, 'fast', 0);
+      await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).performAccessibilityAction('activate');
     });
 
     it(`waitFor (element) with non-elements should throw`, async () => {
diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js
index 7a7f93e7f..5ef6698e0 100644
--- a/detox/test/e2e/03.actions.test.js
+++ b/detox/test/e2e/03.actions.test.js
@@ -26,6 +26,21 @@ describe('Actions', () => {
     await driver.tapsElement.assertTappedOnce();
   });
 
+  it.each([
+    'activate',
+    'magicTap',
+    'escape',
+    'increment',
+    'decrement',
+    'longpress',
+    'custom',
+  ])('should perform %s accessibilityAction', async (actionName) => {
+    await element(by.id('View7991')).performAccessibilityAction(actionName);
+    await expect(
+      element(by.text(`Accessibility Action ${actionName} Working!!!`)),
+    ).toBeVisible();
+  });
+
   describe('multi-tapping', () => {
     it('should multi tap on an element', async () => {
       await driver.tapsElement.multiTap();
diff --git a/detox/test/package.json b/detox/test/package.json
index 3629efa9b..b68bf3acd 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "20.7.0",
+  "version": "20.7.2",
   "private": true,
   "engines": {
     "node": ">=14.5.0"
@@ -55,7 +55,7 @@
     "@typescript-eslint/eslint-plugin": "^5.4.0",
     "@typescript-eslint/parser": "^5.4.0",
     "cross-env": "^7.0.3",
-    "detox": "^20.7.0",
+    "detox": "^20.7.2",
     "eslint": "^7.32.0",
     "eslint-plugin-unicorn": "^39.0.0",
     "execa": "^5.1.1",
diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js
index e8a7b16e0..8ebf46c41 100644
--- a/detox/test/src/Screens/ActionsScreen.js
+++ b/detox/test/src/Screens/ActionsScreen.js
@@ -60,6 +60,17 @@ export default class ActionsScreen extends Component {
 
         <TouchableOpacity onPress={this.onButtonPress.bind(this, 'Tap Working')}
           onLongPress={this.onButtonPress.bind(this, 'Long Press Working')}
+          testID="View7991"
+          accessibilityActions={[
+            { name: 'activate', label: 'activate' },
+            { name: 'increment', label: 'increment' },
+            { name: 'decrement', label: 'decrement' },
+            { name: 'magicTap', label: 'magicTap' },
+            { name: 'escape', label: 'escape' },
+            { name: 'longpress', label: 'longpress' },
+            { name: 'custom', label: 'custom' }
+          ]}
+          onAccessibilityAction={({ nativeEvent: { actionName }}) => this.onButtonPress(`Accessibility Action ${actionName} Working`)}
         >
           <Text style={{ color: 'blue', marginTop: isIos? 40 : 0, marginBottom: 10, textAlign: 'center' }}>Tap Me</Text>
         </TouchableOpacity>