facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
118.34k stars 24.23k forks source link

IOS testID does not create an accessibility id if Views are nested too deep. #30575

Closed ponder6168 closed 1 year ago

ponder6168 commented 3 years ago

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(R) Core(TM) 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.

tomLadder commented 3 years ago

Have the same issue

OneHatRepo commented 3 years ago

Same issue here. Simple app, but the views are nested deeply, and there's nothing I can do to reduce the nesting levels.

NnikK commented 3 years ago

I have the same issue.

aliradd commented 3 years ago

@ponder6168 can we have a rough ETA for this, please? We are running into the same issue too. Thank you.

NnikK commented 3 years ago

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

ponder6168 commented 3 years ago

@aliradd I only reported the issue, I don't work on it. It may turn out to be an appium issue. My company is working with them on the issue. You may want to contact Appium.

kdthomas2121 commented 2 years ago

@ponder6168 did you get anywhere with this issue?

WannaBeDream commented 2 years ago

We've got the same issue

OneHatRepo commented 2 years ago

It's been more than a year with no resolution. This is still biting us.

OneHatRepo commented 2 years ago

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.

OksanaHanailiuk commented 2 years ago

Any update on this? Because we also have a current issue. Thanks

OneHatRepo commented 1 year ago

This is still an issue after almost two years. Makes it so I can't use Appium to test anything but the smallest apps.

OneHatRepo commented 1 year ago

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.

hdesai-dave commented 1 year ago

Yup, same here!

hdesai-dave commented 1 year ago

Should we be logging a bug with react navigation?

OneHatRepo commented 1 year ago

I think react-navigation adds a few levels of nesting, which contributes, but is not the cause of the problem. If I'm using an app with multiple screens, and the layout gets very complex (which often happens), then it's beyond what react-native can handle. Changing react-navigation won't solve this. I think ultimately, it's an issue that react-native needs to deal with.

OneHatRepo commented 1 year ago

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.

hdesai-dave commented 1 year ago

@blavalla what were the findings after accessibility team evaluation

adrianha commented 1 year ago

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.

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 🙇

hdesai-dave commented 1 year ago

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.

lcnpjunior commented 1 year ago

Any updates about this issue guys?

I've tested on Android and got the same issue.

image

adrianha commented 1 year ago

@lcnpjunior could you try to use accessibilityLabel instead of testID on android?

sammy-SC commented 1 year ago

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.

Jitu1888 commented 8 months ago

@sammy-SC We don't have any solution for it.

hdesai-dave commented 8 months ago

@sammy-SC "There seems to be a hard limit on how deeply the view hierarchy is traversed in Appium.:" FYI this is an apple limitation and not appium! React native needs to be smart enough to only add views that are visible to end user to accessibility layer!

sammy-SC commented 8 months ago

thanks @hdesai-dave, I did not know this was a limitation coming from Apple.

React native needs to be smart enough to only add views that are visible to end user to accessibility layer!

I did mention view flattening in my previous answer. Do you think that is not sufficient?

tamaniniandre commented 5 months ago

Hey guys, I created this topic in the Apple's forum, maybe if everyone send some message to bump up this subject, we can get some help from the Apple on it:

https://developer.apple.com/forums/thread/749918

tamaniniandre commented 5 months ago

thanks @hdesai-dave, I did not know this was a limitation coming from Apple.

React native needs to be smart enough to only add views that are visible to end user to accessibility layer!

I did mention view flattening in my previous answer. Do you think that is not sufficient?

not working here :/

OneHatRepo commented 1 month ago

From Apple "Developer Tools Engineer" in that thread linked above:

The 60 depth maximum was added to prevent apps from slowing down system performance when UI queries occur during an XCTest UI test. Unfortunately React Native often creates nested hierarchies that are larger than 60 levels of depth on iOS. That said, you should be able to use webdriverIO for testing, which would not face this 60 depth maximum limit.

marcelowa commented 20 hours ago

From Apple "Developer Tools Engineer" in that thread linked above:

The 60 depth maximum was added to prevent apps from slowing down system performance when UI queries occur during an XCTest UI test. Unfortunately React Native often creates nested hierarchies that are larger than 60 levels of depth on iOS. That said, you should be able to use webdriverIO for testing, which would not face this 60 depth maximum limit.

Can anyone confirm that this fixes the issues ?

tamaniniandre commented 16 hours ago

From Apple "Developer Tools Engineer" in that thread linked above: The 60 depth maximum was added to prevent apps from slowing down system performance when UI queries occur during an XCTest UI test. Unfortunately React Native often creates nested hierarchies that are larger than 60 levels of depth on iOS. That said, you should be able to use webdriverIO for testing, which would not face this 60 depth maximum limit.

Can anyone confirm that this fixes the issues ?

I already tested this (60 levels), but this did not solve our issue. In some screens it did actually, but the more details the page has, the more nested elements the React native creates, and I could not automate most of our screens. It did not solve for us. If your app has few details, maybe it would work.