DEV Community

Ricardo Barbosa
Ricardo Barbosa

Posted on • Updated on

An end to the abuse of Accessibility IDs

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 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. 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 testIDs, 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!

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;

Enter fullscreen mode Exit fullscreen mode

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

So, what changes?

Declaring testing attributes

React Native <= 0.63

<Button
  icon={icon}
  text={t('login')} // Text inside button, translated
  accessibilityLabel="Login" // What's read out loud to user
>
  // ...
</Button>
Enter fullscreen mode Exit fullscreen mode
  • 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

<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>
Enter fullscreen mode Exit fullscreen mode
  • 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 testIDs 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)

// 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")}`)
Enter fullscreen mode Exit fullscreen mode

Java

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

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

Enter fullscreen mode Exit fullscreen mode

Python

// Before
el = self.driver.find_element_by_accessibility_id('Login')

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

Enter fullscreen mode Exit fullscreen mode

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

Discussion (7)

Collapse
hdesaidave profile image
hdesai-dave • Edited on

Hi i am able to find element with xpath but not id for an element where i added testID using webdriverio.

selector = $(//*[@resource-id="bank"]); works

selector = $(id:bank); doesn't work

The way you suggested in post

selector = $(id=bank) also doesn't work.

Collapse
nextlevelbeard profile image
Ricardo Barbosa Author

It seems it's a problem with Appium, it modifies your selector and adds the package name automatically on Android. In short, finding the element fails because you didn't specify the package name in the testID of your app. I've updated the article with some more info and a technical solution, hope it helps!

Collapse
hdesaidave profile image
hdesai-dave

Thanks man this worked but wondering why would react native not implement this in the framework themselves?

Collapse
retyui profile image
David Narbutovich

Hack with accessibilityLabel instead of testID on Android is not actual since react-native@0.65.x !!!

See changelog: github.com/react-native-community/... ;

Commit: github.com/facebook/react-native/c... ;

Collapse
nextlevelbeard profile image
Ricardo Barbosa Author

Seems it's just a case of the changelog not being correctly generated.
I believe testID is available since 0.64.

Collapse
vitalyiegorov profile image
Vitaly Iegorov • Edited on

Does this work for "Text" elements on IOS? In our case Appium does not see Text testID="..."

Collapse
nextlevelbeard profile image
Ricardo Barbosa Author • Edited on

That should indeed be the case yes, for whatever React Native version.
If it's not working make sure of two things:

  1. If any parent of the React Native Text component is a Touchable (TouchableOpacity for example) then you need to make sure to add accessible={false} to this parent component in order for you to be able to see this component's children like the Text. This happens because the default accessible value for Touchable components is true.
  2. You might have a very detailed screen with lots of elements and Appium at one point just gives up going down the tree and does not show every element. To avoid this, use Appium's Settings API by adding the capabilities appium:settings[snapshotMaxDepth] and appium:settings[customSnapshotTimeout] to your tests. Increase their default values to have more elements appear when finding strategies. Have a look here for the default values.