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

getByRole should accept a second argument to refine query as in react-testing-library

Open leoparis89 opened this issue 4 years ago • 10 comments

Describe the Feature

Should be able to query a Button element as follows:

getByRole('button', {name: /submit/i})

At the present time, this is impossible so I have to add redundant accessiblityLabel props, for example:

<Button accessibilityLabel="submit-button">Submit</Button>

And then, use queryByA11yLabel('submit-button')

leoparis89 avatar Sep 27 '21 16:09 leoparis89

Definitely, would welcome such a feature :)

thymikee avatar Sep 27 '21 19:09 thymikee

Hello @thymikee, @leoparis89 I want to contribute to this package. So, I am walking through the issues. Do you mind me asking, What would we benefit from having this feature? Because we can already simply access the elements by their value with getByText. Thanks :)

guvenkaranfil avatar Oct 13 '21 19:10 guvenkaranfil

One thing would be mimicking React Testing Library API, which is a good thing. Another thing is that you could test 2 things with one query. To stay with the example, a single query would ensure that there's a button with a certain text. Right now you'd need to perform 2 queries and you wouldn't be sure if you actually hit the same element, unless you compare them by reference or check the props, which is not ideal.

thymikee avatar Oct 14 '21 08:10 thymikee

I would like to work on this. I will mimic React Testing Library API. I will post a plan once I have one

kiranjd avatar Oct 26 '21 03:10 kiranjd

https://github.com/callstack/react-native-testing-library/blob/34fc03a8c936aa38f91e3c2b042363f964c711c3/src/helpers/makeA11yQuery.js#L50

This seems to be the place where we will need to add the matcher for second arguments(like {name: 'press-me'}). This might needs to be added in all possible queries(getBy, findBy, etc.,)

kiranjd avatar Nov 08 '21 17:11 kiranjd

@kiranjd still interested? :)

thymikee avatar Nov 23 '21 21:11 thymikee

Yes, @thymikee. I was able to get it to work using getNodeByText function in byText.js file. Currently trying write tests to see if it works for all cases.

kiranjd avatar Nov 24 '21 07:11 kiranjd

@thymikee Here's what I'am trying to do:

  1. Get matching nodes from getByRole
  2. For each matching role, if name is passed, then use getQueriesForElement to get matchers for it's children
  3. return the result of getQueriesForElement(node).queryByText(options.name); as we are trying to match for text

Here's a working code for couple tests that I made in makeA11yQuery.js: 46:

  const getBy = (matcher: M, options?: QueryOptions) => {
    try {
      if (options?.name) {
        return instance.find((node) => {
          const matchesRole =
            isNodeValid(node) && matcherFn(node.props[name], matcher);

          if (!matchesRole) return false;

          return !!getQueriesForElement(node).queryByText(options.name);
        });
      }

      return instance.find(
        (node) => isNodeValid(node) && matcherFn(node.props[name], matcher)
      );
    } catch (error) {
      throw new ErrorWithStack(
        prepareErrorMessage(error, name, matcher),
        getBy
      );
    }
  };

I still have to cover all other queries as well. But, this is the core of it. Would love to hear your feedback and proceed further :)

kiranjd avatar Nov 24 '21 17:11 kiranjd

A thing you'll need to consider is that if you match by name, you should also match by label text, since you should definitely match <Button accessibilityLabel="exit" onPress={...}><Icon type="door" /></Button> with getByRole('button', {name: 'exit'}).

Not necessarily relevant but which might be interesting to you, we've reimplemented that in userland in our codebase. As you can see the reasoning is the same (first find the element with the role, then try to match the name in its children). Since it does work for us (and does seem to work as well as web), I guess your implementation should do the job.

const queryByAccessibleName = (renderApi: Queries, name: string) =>
  renderApi.queryByLabelText(name) || renderApi.queryByText(name)

const getByRole = (role: AccessibilityRole, options: { name?: string } = {}) => {
      const elements = queryAllByAccessibleName(renderResult, name)

      const e = new Error(
        `Unable to find an accessible element with the role "${role}" and name "${name}"`
      )
      e.name = 'TestingLibraryElementError'
      Error.captureStackTrace(e, getByRole)

      if (elements.length === 0) {
        throw e
      }

      const elementWithRole = elements.find((element) => element.props.accessibilityRole === role)
      if (elementWithRole) {
        return elementWithRole
      }

      // a text can be nested within a button, so the accessibilityRole wouldn't be on the same
      // element
      try {
        const roledElements = renderResult.getAllByRole(role)
        for (const roledElement of roledElements) {
          const withinRoledElement = nativeWithin(roledElement)
          const labelledTextWithinRoledElement = queryByAccessibleName(withinRoledElement, name)
          if (labelledTextWithinRoledElement) {
            return labelledTextWithinRoledElement
          }
        }
        return
      } catch {
        throw e
      }

      throw e
}

AugustinLF avatar Nov 25 '21 09:11 AugustinLF

@thymikee Issued a draft PR. Appreciate your feedback

kiranjd avatar Dec 02 '21 14:12 kiranjd