react-native-testing-library icon indicating copy to clipboard operation
react-native-testing-library copied to clipboard

Why are accessibilty elements only text, textInput, and switch?

Open KrastanD opened this issue 1 year ago • 12 comments

Ask your Question

I am using Tamagui and I want to check that a radio button has been checked but it's not able to and I think it's because Tamagui adds the radio role to a view but React Native Testing Library only checks text, textInput and switch elements for a role.

Could you explain the reasoning for having only those elements as accessibility elements? Do you have a recommended way that I can check that the radio button has been checked, or does Tamagui have to change how they set role?

KrastanD avatar Feb 22 '24 17:02 KrastanD

To make a View an accessibility element just pass accessible={true} props to it.

The elements types you mention: Text, TextInput and Switch are considered to be accessible by default (even without accessible={true} prop).

Not sure what happens in Tamagui, as the code is pretty abstract. If you think that RNTL is missing something, the create a minimal Expo app using Tamagui that showcases the missing case. Having something like this at hand would allow me verify whether to tweak our simulation code.

mdjastrzebski avatar Feb 22 '24 19:02 mdjastrzebski

Adding accessible did make the element selectable, thank you!!! Idk how I missed that line.

I created an example expo app, but my test using userEvent wasn't checking the component. I tried all kinds of selectors but that didn't make a difference. Out of desperation I tried using fireEvent and that worked. Could you take a look?

My hunch (from my understanding) is that fireEvent checks the parents handlers until it finds one, and userEvent doesn't do that so here, the onValueChange is on the RadioGroup level, not the RadioGroupItem so userEvent can't trigger it. I'm not sure how to trigger it with userEvent since I need to click on the specific item.

KrastanD avatar Feb 28 '24 04:02 KrastanD

Just ran into this myself! I believe Tamagui should handle adding accessible to components, right? React Native Elements takes care of this for you.

tlmader avatar Mar 01 '24 00:03 tlmader

@KrastanD User Event has code code that performs lookup for pressable element from given element up: https://github.com/callstack/react-native-testing-library/blob/4e148bdb0573fcb86686482c3af1d1e02e1cc937/src/user-event/press/press.ts#L58

From that point it locks on that element, and tries to send all of touch events there without further lookups.

mdjastrzebski avatar Mar 12 '24 08:03 mdjastrzebski

@KrastanD I've reviewed this issue to check if it can be closed as it looked stale. I've run your repro repository and found it interesting. When I run screen.debug() on our failing test, it does not seems to contain onPress or similar event handler:

<View>
      <View
        __scopeRovingFocusGroup="RadioGroup"
        loop={true}
      >
        <View
          accessibilityRole="radiogroup"
        >
          <View>
            <View
              __scopeRovingFocusGroup="RadioGroup"
              accessibilityRole="radio"
              accessibilityState={
                {
                  "checked": false,
                }
              }
              accessible={true}
              active={false}
              focusable={true}
              id="radiogroup-1"
            />
            <View
              pointerEvents="none"
            />
            <Text
              htmlFor="radiogroup-1"
              id=":r3:"
              suppressHighlighting={true}
              userSelect="none"
            >
              First value
            </Text>
          </View>
          <View>
            <View
              __scopeRovingFocusGroup="RadioGroup"
              accessibilityRole="radio"
              accessibilityState={
                {
                  "checked": false,
                }
              }
              accessible={true}
              active={false}
              focusable={true}
              id="radiogroup-2"
            />
            <View
              pointerEvents="none"
            />
            <Text
              htmlFor="radiogroup-2"
              id=":r4:"
              suppressHighlighting={true}
              userSelect="none"
            >
              Second value
            </Text>
          </View>
          <View>
            <View
              __scopeRovingFocusGroup="RadioGroup"
              accessibilityRole="radio"
              accessibilityState={
                {
                  "checked": false,
                }
              }
              accessible={true}
              active={false}
              focusable={true}
              id="radiogroup-3"
            />
            <View
              pointerEvents="none"
            />
            <Text
              htmlFor="radiogroup-3"
              id=":r5:"
              suppressHighlighting={true}
              userSelect="none"
            >
              Third value
            </Text>
          </View>
        </View>
      </View>
    </View>

screen.debug displays only host views, and it seems that none of these views actually has any event handler 🧐 This will make userEvent.press fail as it expects onPress or onPressIn/onPressOut to be present. The reason why Fire Event works is that it invokes event handlers on both host and composite components, and Tamagui's RadioGroup seems to contain such onPress handler on RadioGroupItemFrame.

Tamagui is quite complex as it uses some additional abstractions, so it's hard for me to say what exactly is happening there. The thing that I can see, is that the host element tree does not seem to contain any event handlers (onXxx) at all. Perhaps this is supposed to be selected by the following effect code in React Native 🤷‍♂️

Let me know if you are willing to explore this more or should we close this issue.

mdjastrzebski avatar Apr 26 '24 20:04 mdjastrzebski

I am on the exact same situation, after hours of research I found that using the accesible attribute works fine and I'm thinking on adding a wrapper for all these elements from Tamagui that need it.

I wasn't very happy but I think that's the only solution, then I found that only fireEvent works instead of user event.

My concern is, can we rely on all these and be in peace with having the wrapper plus using fire event? It's messing with my head a little bit since I have the feeling that fireEvent will be away eventually, perhaps I'm wrong on that.

Thanks

mfrfinbox avatar Apr 30 '24 20:04 mfrfinbox

@mfrfinbox you should raise this issue with Tamagui maintainers. Their code is pretty complex due to abstractions and RN/web shared code, so they should be the best people to consult on this.

From my perspective the issue is on Tamagui's side (lack of accessible prop) but I would be happy to be proven wrong 😊

mdjastrzebski avatar May 01 '24 16:05 mdjastrzebski

For the accessible={true}, that should be contributed to tamagui directly imo so everyone gets it out the box and don't run into it like @mfrfinbox and I have.

As for the host elements not showing onPress, I'm not quite sure how that happens. Aren't all composite elements built from host elements? The RadioGroupItemFrame has the onPress and extends a ThemableStack, which extends a YStack, which extends a View.

KrastanD avatar May 01 '24 16:05 KrastanD

I've open the issues on the Tamagui side, thanks

https://github.com/tamagui/tamagui/issues/2613 https://github.com/tamagui/tamagui/issues/2614

mfrfinbox avatar May 01 '24 17:05 mfrfinbox

@mfrfinbox thank's for logging the issues, please update them with MyComponent source, as otherwise they are incomplete.

mdjastrzebski avatar May 02 '24 08:05 mdjastrzebski

@KrastanD as for host elements, in React the components are not being extended (subclassed) in classic OOP way, they are being rendered (composed) by other (composite) components. The YStack is a composite component that renders View, and should pass all it's props there as it uses styled, however for some reasons onPress from RadioGroupItemFrame does not get to the final View host element.

The reason why User Events insist on invoking only events on the host components, is that these are the only components that the user can see as they correspond to UI controls in iOS/Android. Composite components are just an abstraction, and cannot be directly observed or interacted by the users.

mdjastrzebski avatar May 02 '24 08:05 mdjastrzebski

@mfrfinbox thank's for logging the issues, please update them with MyComponent source, as otherwise they are incomplete.

Done!

mfrfinbox avatar May 02 '24 08:05 mfrfinbox

Closing as it requires fixing upstream (Tamagui)

mdjastrzebski avatar May 23 '24 11:05 mdjastrzebski