react-native: IOS testID does not create an accessibility id if Views are nested too deep.

Description

If you nest <View>s too deeply adding the code ...{ testID: 'idValue'} to the <View> does not produce an accessibility id on the element. For example, I placed the following code in my App.js file

import React, { PureComponent } from 'react';
import { View, Text, StyleSheet } from 'react-native';

const style = StyleSheet.create({
  container: {
    alignItems: 'center',
    backgroundColor: '#007DBA',
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
  },
});

export default class App extends PureComponent {
  render() {
    return (
      <View { ...{ testID: 'outer' } } style={ style.container }>
        <Text { ...{ testID: 'text1' } } >text1</Text>
        <View { ...{ testID: 'inner1' } } >
          <Text{ ...{ testID: 'inner1_text' } }>inner1 text</Text>
          <View { ...{ testID: 'inner2' } } >
            <Text{ ...{ testID: 'inner2_text' } }>inner2 text</Text>
            <View { ...{ testID: 'inner3' } } >
              <Text{ ...{ testID: 'inner3_text' } }>inner3 text</Text>
              <View { ...{ testID: 'inner4' } } >
                <Text{ ...{ testID: 'inner4_text' } }>inner4 text</Text>
                <View { ...{ testID: 'inner5' } } >
                  <Text{ ...{ testID: 'inner5_text' } }>inner5 text</Text>
                  <View { ...{ testID: 'inner6' } } >
                    <Text{ ...{ testID: 'inner6_text' } }>inner6 text</Text>
                    <View { ...{ testID: 'inner7' } } >
                      <Text{ ...{ testID: 'inner7_text' } }>inner7 text</Text>
                      <View { ...{ testID: 'inner8' } } >
                        <Text{ ...{ testID: 'inner8_text' } }>inner8 text</Text>
                        <View { ...{ testID: 'inner9' } } >
                          <Text{ ...{ testID: 'inner9_text' } }>inner9 text</Text>
                          <View { ...{ testID: 'inner10' } } >
                            <Text{ ...{ testID: 'inner10_text' } }>inner10 text</Text>
                            <View { ...{ testID: 'inner11' } } >
                              <Text{ ...{ testID: 'inner11_text' } }>inner11 text</Text>
                              <View { ...{ testID: 'inner12' } } >
                                <Text{ ...{ testID: 'inner12_text' } }>inner12 text</Text>
                                <View { ...{ testID: 'inner13' } } >
                                  <Text{ ...{ testID: 'inner13_text' } }>inner13 text</Text>
                                  <View { ...{ testID: 'inner14' } } >
                                    <Text{ ...{ testID: 'inner14_text' } }>inner14 text</Text>
                                    <View { ...{ testID: 'inner15' } } >
                                      <Text{ ...{ testID: 'inner15_text' } }>inner15 text</Text>
                                      <View { ...{ testID: 'inner16' } } >
                                        <Text{ ...{ testID: 'inner16_text' } }>inner16 text</Text>
                                        <View { ...{ testID: 'inner17' } } >
                                          <Text{ ...{ testID: 'inner17_text' } }>inner17 text</Text>
                                          <View { ...{ testID: 'inner18' } } >
                                            <Text{ ...{ testID: 'inner18_text' } }>inner18 text</Text>
                                            <View { ...{ testID: 'inner19' } } >
                                              <Text{ ...{ testID: 'inner19_text' } }>inner19 text</Text>
                                              <View { ...{ testID: 'inner20' } } >
                                                <Text{ ...{ testID: 'inner20_text' } }>inner20 text</Text>
                                                <View { ...{ testID: 'inner21' } } >
                                                  <Text{ ...{ testID: 'inner21_text' } }>inner21 text</Text>
                                                  <View { ...{ testID: 'inner22' } } >
                                                    <Text{ ...{ testID: 'inner22_text' } }>inner22 text</Text>
                                                    <View { ...{ testID: 'inner23' } } >
                                                      <Text{ ...{ testID: 'inner23_text' } }>inner23 text</Text>
                                                      <View { ...{ testID: 'inner24' } } >
                                                        <Text{ ...{ testID: 'inner24_text' } }>inner24 text</Text>
                                                        <View { ...{ testID: 'inner25' } } >
                                                          <Text{ ...{ testID: 'inner25_text' } }>inner25 text</Text>
                                                          <View { ...{ testID: 'inner26' } } >
                                                            <Text{ ...{ testID: 'inner26_text' } }>inner26 text</Text>
                                                            <View { ...{ testID: 'inner27' } } >
                                                              <Text{ ...{ testID: 'inner27_text' } }>inner27 text</Text>
                                                              <View { ...{ testID: 'inner28' } } >
                                                                <Text{ ...{ testID: 'inner28_text' } }>inner28 text</Text>
                                                                <View { ...{ testID: 'inner29' } } >
                                                                  <Text{ ...{ testID: 'inner29_text' } }>inner29 text</Text>
                                                                  <View { ...{ testID: 'inner30' } } >
                                                                    <Text{ ...{ testID: 'inner30_text' } }>inner30 text</Text>
                                                                    <View { ...{ testID: 'inner31' } } >
                                                                      <Text{ ...{ testID: 'inner31_text' } }>inner31 text</Text>
                                                                      <View { ...{ testID: 'inner32' } } >
                                                                        <Text{ ...{ testID: 'inner32_text' } }>inner32 text</Text>
                                                                        <View { ...{ testID: 'inner33' } } >
                                                                          <Text{ ...{ testID: 'inner33_text' } }>inner33 text</Text>
                                                                          <View { ...{ testID: 'inner34' } } >
                                                                            <Text{ ...{ testID: 'inner34_text' } }>inner34 text</Text>
                                                                            <View { ...{ testID: 'inner35' } } >
                                                                              <Text{ ...{ testID: 'inner35_text' } }>inner35 text</Text>
                                                                              <View { ...{ testID: 'inner36' } } >
                                                                                <Text{ ...{ testID: 'inner36_text' } }>inner36 text</Text>
                                                                                <View { ...{ testID: 'inner37' } } >
                                                                                  <Text{ ...{ testID: 'inner37_text' } }>inner37 text</Text>
                                                                                  <View { ...{ testID: 'inner38' } } >
                                                                                    <Text{ ...{ testID: 'inner38_text' } }>inner38 text</Text>
                                                                                    <View { ...{ testID: 'inner39' } } >
                                                                                      <Text{ ...{ testID: 'inner39_text' } }>inner39 text</Text>
                                                                                      <View { ...{ testID: 'inner40' } } >
                                                                                        <Text{ ...{ testID: 'inner40_text' } }>inner40 text</Text>
                                                                                        <View { ...{ testID: 'inner41' } } >
                                                                                          <Text{ ...{ testID: 'inner41_text' } }>inner41 text</Text>
                                                                                          <View { ...{ testID: 'inner42' } } >
                                                                                            <Text{ ...{ testID: 'inner42_text' } }>inner42 text</Text>
                                                                                            <View { ...{ testID: 'inner43' } } >
                                                                                              <Text{ ...{ testID: 'inner43_text' } }>inner43 text</Text>
                                                                                              <View { ...{ testID: 'inner44' } } >
                                                                                                <Text{ ...{ testID: 'inner44_text' } }>inner44 text</Text>
                                                                                                <View { ...{ testID: 'inner45' } } >
                                                                                                  <Text{ ...{ testID: 'inner45_text' } }>inner45 text</Text>
                                                                                                  <View { ...{ testID: 'inner46' } } >
                                                                                                    <Text{ ...{ testID: 'inner46_text' } }>inner46 text</Text>
                                                                                                  </View>
                                                                                                </View>
                                                                                              </View>
                                                                                            </View>
                                                                                          </View>
                                                                                        </View>
                                                                                      </View>
                                                                                    </View>
                                                                                  </View>
                                                                                </View>
                                                                              </View>
                                                                            </View>
                                                                          </View>
                                                                        </View>
                                                                      </View>
                                                                    </View>
                                                                  </View>
                                                                </View>
                                                              </View>
                                                            </View>
                                                          </View>
                                                        </View>
                                                      </View>
                                                    </View>
                                                  </View>
                                                </View>
                                              </View>
                                            </View>
                                          </View>
                                        </View>
                                      </View>
                                    </View>
                                  </View>
                                </View>
                              </View>
                            </View>
                          </View>
                        </View>
                      </View>
                    </View>
                  </View>
                </View>
              </View>
            </View>
          </View>
        </View>
      </View>
    );
  }
}

When I examine the code with the Appium inspector, the last <View> with a working accessibility id is the <View> with testID “inner41”. image

The <View> with testID “inner42” shows an accessibility id but the id is not accessible and an “Interactions are not available for this element” message appears.

image

None of the other <View>s nested under the <View> with testId “inner42” display.

React Native version:

System: OS: macOS 10.15.7 CPU: (12) x64 Intel® Core™ i7-8850H CPU @ 2.60GHz Memory: 42.29 MB / 16.00 GB Shell: 5.7.1 - /bin/zsh Binaries: Node: 12.18.4 - /usr/local/bin/node npm: 6.14.6 - /usr/local/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman SDKs: iOS SDK: Platforms: iOS 14.0, DriverKit 19.0, macOS 10.15, tvOS 14.0, watchOS 7.0 Android SDK: API Levels: 28, 29, 30 Build Tools: 28.0.3, 29.0.2, 30.0.2 System Images: android-28 | Google Play Intel x86 Atom, android-29 | Google Play Intel x86 Atom, android-30 | Google Play Intel x86 Atom IDEs: Android Studio: 4.0 AI-193.6911.18.40.6626763 Xcode: 12.0.1/12A7300 - /usr/bin/xcodebuild npmPackages: react: 16.8.6 => 16.8.6 react-native: 0.60.6 => 0.60.6 npmGlobalPackages: react-native-cli: 2.0.1

Steps To Reproduce

Provide a detailed list of steps that reproduce the issue.

  1. Create a page of nested <View>s nested 50 deep.
  2. Examine the page with Appium inspector.

Expected Results

We need the nesting limit to be deeper. We are bumping up against the limit in our Appium tests. This does not effect normal app usage by a person.

Snack, code example, screenshot, or link to a repository:

A code example and screenshot are presented above.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 7
  • Comments: 28 (4 by maintainers)

Most upvoted comments

Adding snapshotMaxDepth to capabilities helps: "settings[snapshotMaxDepth]": 60

I wish there was a way to bump this ticket. My guess is that it’s been forgotten and/or ignored for so long, it won’t ever get addressed.

I wonder if these have something to do with it:

https://github.com/appium/appium/issues/15687#issuecomment-891793558 “This is known XCTest issue and we cannot do nothing from Appium side to change that behaviour. Only Apple itself could change the behaviour there. Closed as duplicate.”

https://github.com/appium/appium/issues/14825#issuecomment-881212123 “… the problem is not the settings themselves. The problem on iOS seems to be that for a large number of subviews there is a point where the web driver agent fails to provide the whole application view hierarchy. In our case is when we go over 63 nested views.”

https://github.com/appium/appium/issues/14825#issuecomment-960537920 “…I believe that this is either an XCTest or React bug, however given that the error comes from CoreFoundation I lean towards the latter. And in any case, other than providing the option to limit the depth of the tree - which is already available - I don’t see a way in which this issue can be circumvented from the Appium end of things.”

So it looks like limiting view nesting levels is the only solution right now.

This problem cannot be solved from React Native. Even if the app was built directly with UIKit, the problem wouldn’t go away. There seems to be a hard limit on how deeply the view hierarchy is traversed in Appium. I’m not familiar with how Appium implemented view traversal. From the examples it looks like React Native inserts all of the views, it doesn’t cut them off at arbitrary point.

Thing to note, adding testID/accessibilityLabel prevents view from being flattened. View flattening is not a feature that will fix this directly but it might make the problem less frequent because it makes view hierarchy less deep. In the New Architecture, we are bringing view flattening to iOS as well: https://github.com/reactwg/react-native-new-architecture/discussions/110. But again, it wouldn’t fix the example in this issue where each view has testID.

Just tried this nested views behavior in React Native 0.71.1 and the result is still the same with the OP, if the views are too deep, it can’t be found in the appium inspector element tree.

  • Nested Views nested-views

  • Appium Inspector appium-inspector

  • Appium Inspectore Element Tree appium-element-tree

here’s the code for the example:

function App() {
  const renderTestComponent = (id: number = 1) => {
    if (id > 50) {
      return null;
    }

    return (
      <View
        key={id}
        testID={`testID.${id}`}
        style={{
          backgroundColor: id % 2 === 0 ? 'lightblue' : 'orange',
          padding: 2,
        }}>
        <Pressable onPress={() => alert(id)}>
          <Text style={{textAlign: 'center'}}>{id.toString()}</Text>
        </Pressable>
        {renderTestComponent(id + 1)}
      </View>
    );
  };

  return (
    <ScrollView>
      {renderTestComponent()}
    </ScrollView>
  );
}

any reference or guide for fixing this issue? thank you 🙇

Just tried this nested views behavior in React Native 0.71.1 and the result is still the same with the OP, if the views are too deep, it can’t be found in the appium inspector element tree.

  • Nested Views
nested-views
  • Appium Inspector
appium-inspector
  • Appium Inspectore Element Tree
appium-element-tree

here’s the code for the example:

function App() {
  const renderTestComponent = (id: number = 1) => {
    if (id > 50) {
      return null;
    }

    return (
      <View
        key={id}
        testID={`testID.${id}`}
        style={{
          backgroundColor: id % 2 === 0 ? 'lightblue' : 'orange',
          padding: 2,
        }}>
        <Pressable onPress={() => alert(id)}>
          <Text style={{textAlign: 'center'}}>{id.toString()}</Text>
        </Pressable>
        {renderTestComponent(id + 1)}
      </View>
    );
  };

  return (
    <ScrollView>
      {renderTestComponent()}
    </ScrollView>
  );
}

any reference or guide for fixing this issue? thank you 🙇

That’s really sad news. We are going to upgrade react native here shortly as well. This tells me, I should not be hopeful! Isn’t this a serious accessibility problem as everyone has pointed out for community to prioritize? The dom on iOS is horrendously large and 90% of elements are hidden.

By the way, it’s not just testID—it’s any attribute at all. If the levels are nested too deeply, the attributes cannot be targeted, so I can’t even use things like rendered text, as recommended here.