An end to the abuse of Accessibility IDs
Tags: reactnative, testing, mobile, a11y
Published: 2021-03-15 10:21:26

As of **React Native 0.64**, we can and should change the standard way of specifying the attributes used for test automation purposes with Appium.

Since the dawn of time, the test automation community has been using and abusing the **Accessibility ID** for the purposes of tagging and finding the elements and components of React Native apps though Appium.

Why is this though? We've long had the ability to specify a `testID` in React Native components for quite a while now, the problem was that **Appium had no way of retrieving this attribute on Android apps**. Therefore we resorted to using the Accessibility ID for this purpose, something that IS retrievable.

A long standing problem with this method? **It was sacrificing proper accessibility for your app by forcing you to maintain static accessibility labels.** This can finally come to an end because the `testID` attribute [will now be exposed on Android builds](https://github.com/facebook/react-native/pull/29610) as the also very retrievable `resource-id`!

# Moving strategies

Since we are moving away from using `accessibilityLabel` in favor of `testID`, we need to change our strategy to find the elements during automation. The switch is simple. The previous strategy is usually shorthanded to `accessibility-id` by many frameworks. The new one is simply `id`.

On iOS, both of these strategies are identical: they first try to match the `name` attribute and fall back to matching the `label` attribute. If per chance you'd like to set both `accessibilityLabel` and the `testID` props you should know they correspond to the `label` and `name` element attributes respectively. This is easy to observe using Appium Desktop.

On Android however these two strategies differ. Instead of using the `accessibility-id` strategy and trying to find elements that match the `content-desc` attribute, we must now use the `id` strategy that attempts to match the `resource-id` element attribute.

In many frameworks, this change should be trivial, but [there is a catch](https://github.com/appium/appium/issues/15354). It seems Appium requires the package name to be part of the `resource-id` itself otherwise it will fail to find it. If your package identifier is `com.example` and you want your `testID` to be `landing-login`, your should declare it as `com.example:id/landing-login`.

It seems the best way to guarantee everything works is to prefix the package name to the `testID`s, it's a good practice for Android development after all. Let's add an helper function to our React Native app called `getTestID` or `tID` for short!

```jsx
import { Platform } from 'react-native';
import { getBundleId } from 'react-native-device-info';

const appIdentifier = getBundleId();

export function getTestID(testID) {
  if (!testID) {
    return undefined;
  }

  const prefix = `${appIdentifier}:id/`;
  const hasPrefix = testID.startsWith(prefix);

  return Platform.select({
    android: !hasPrefix ? `${prefix}${testID}` : testID,
    ios: hasPrefix ? testID.slice(prefix.length) : testID,
  });
}

export const tID = getTestID;

```

Now that we have this helper at our disposal, we can freely use it.

So, what changes?

# Declaring testing attributes

### React Native <= 0.63
```jsx
<Button
  icon={icon}
  text={t('login')} // Text inside button, translated
  accessibilityLabel="Login" // What's read out loud to user
>
  // ...
</Button>
```

- We needed to set a static Accessibility ID to be able to find the element in our (Android) end-to-end tests
- Android's TalkBack or iOS's Voice Over would read out a static text for all languages that your app supports, "Login" in this case

### React Native >= 0.64

```jsx
<Button
  icon={icon}
  text={t('login')} // Text inside button, translated
  accessibilityLabel={t('login')} // What's read out loud to user, now also translated
  testID={tID("landing-login")}
>
  // ...
</Button>
```
- We no longer need to set a static Accessibility ID to be able to find the element in our (Android) end-to-end tests, we use the `testID` attribute with the help of a `tID` helper function
- Android's TalkBack or iOS's Voice Over will read out the proper text out loud regardless of the language the user has set

# Finding the element

Now that our `testID`s are built into the app let's find them.
It looks like Appium adds the package name to the selector if it is missing, we can simply use the unique portion when finding the elements. If for some reason it's not working, do add the package name to the selector. This will not work in Appium Desktop, for example.

### JavaScript ([webdriverIO](https://webdriver.io))
```js
// Helper in case you want to add the package name
// Should not be needed
const prefix = (selector) => {
  if(browser.isAndroid){
    const { appPackage, 'appium:appPackage': appiumAppPackage } = browser.capabilities;
    return `${appPackage || appiumAppPackage}:id/${selector}`;
  }
  return selector;
};

// Before
const loginBtn = $("~Login") // ~ uses accessibility-id strategy

// After
const loginBtn = $("id=landing-login") // # uses id strategy
// or
const loginBtn = $(`id=${prefix("landing-login")}`)
```

### Java
```java
// Before
List<MobileElement> loginBtn = (List<MobileElement>) driver.findElementByAccessibilityId("Login");

// After
List<MobileElement> loginBtn = driver.findElementByID("landing-login");

```

### Python
```java
// Before
el = self.driver.find_element_by_accessibility_id('Login')

// After
el = self.driver.find_element_by_id('landing-login')

```

That's all there is to it.
Let's give accessibility a break, shall we?