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 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!
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
<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
<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 atID
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)
// 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
// Before
List<MobileElement> loginBtn = (List<MobileElement>) driver.findElementByAccessibilityId("Login");
// After
List<MobileElement> loginBtn = driver.findElementByID("landing-login");
Python
// 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?
Top comments (12)
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"]
); worksselector = $(
id:bank
); doesn't workThe way you suggested in post
selector = $(
id=bank
) also doesn't work.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!
Thanks man this worked but wondering why would react native not implement this in the framework themselves?
Hack with
accessibilityLabel
instead oftestID
on Android is not actual sincereact-native@0.65.x
!!!See changelog: github.com/react-native-community/... ;
Commit: github.com/facebook/react-native/c... ;
Seems it's just a case of the changelog not being correctly generated.
I believe
testID
is available since 0.64.In my case, I was not able to find appPackage from
browser.capabilities
. Instead of using app package, we can use testid=test
as prefix to make it work which works for me.We can create prefix like this:
export const prefix = (selector: string) => {
if (driver.isAndroid) {
return
id=test:id/${selector}
;}
return
~${selector}
;};
we are using testproject (appium) based, and it cannot detect id on IOS even though ive added testID to the element, the catch is , this is only happening on item inside list (flatlist), label becoming the text instead of testID
It works good on iOS. For Android with testID, resource-id gets set for Views, Text, Buttons but not for elements like TextInput etc. So need to use accessibilityLabel in React Native for accessibility id on Appium side
Hey, I have an issue with TestID for Android. TestID assigned to TextInput control not working.
Any suggestions how to fix it?
I will be very grateful for your helpΡ Thanks
github.com/facebook/react-native/i...
Does this work for "Text" elements on IOS? In our case Appium does not see Text testID="..."
That should indeed be the case yes, for whatever React Native version.
If it's not working make sure of two things:
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 defaultaccessible
value forTouchable
components istrue
.appium:settings[snapshotMaxDepth]
andappium: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.Does anyone know how this approach affects performance of the react native application? Or how to measure that?