Skip to content

Add getAllByTestId and generic getBy and getAllBy queries #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from

Conversation

mzdunek93
Copy link

Summary

I don't know who put getAllByTestId and queryAllByTestId in the documentation when they don't actually exist, but that's a huge omission and I had to fix it. But that was easy

The bigger problem is that react test renderer acts idiotically when you try to search anything by props - for example, if you put one Text element with a given testID in your hierarchy and try to search by that ID, it returns two components, one which has the type equal to the Text component, and one for which the type is simply the string "Text" - it turns out that the internal hierarchy of react test renderer does not quite look like the one that you code. I don't have enough determination to find out why it is so, but I know that an easy way to fix this is to allow users to filter their searches by both testID and the component's type. Right now I can't do that easily with your package, I'd have to filter my components manually. So I created the generic queries getBy and getAllBy, two which the programmer can pass two types of arguments:

  • a simple test accepting a ReactTestInstance, just like the one you would pass to the functions find() and findAll() from react test renderer
  • more interestingly, an arbitrary object that matches on the instance properties, for example { type: Text, props: { style: { color: 'red' } }, testID: 'label' } will search for a text node which has color red in its style object and a "label" testID. Now, testID doesn't actually exist on a test instance, so if you search by { testID: 'something' } it automatically gets converted to { props: { testID: 'something' } }.

This is obviously a great feature and should be merged. Pozdrawiam.

@mzdunek93 mzdunek93 force-pushed the master branch 2 times, most recently from acb01c7 to 107867e Compare June 2, 2019 18:50
Copy link
Member

@thymikee thymikee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for tackling this! Unfortunately, we don't want to add more queries and helpers other than getAllByTestId currently. I described the issue you encountered in one of the comments, hope that's a bit clearer now.

export const queryByAPI = (instance: ReactTestInstance) => ({
queryBy: queryBy(instance),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this

queryByTestId: queryByTestId(instance),
queryByName: queryByName(instance),
queryByType: queryByType(instance),
queryByText: queryByText(instance),
queryByPlaceholder: queryByPlaceholder(instance),
queryByProps: queryByProps(instance),
queryAllBy: queryAllBy(instance),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this as well (and all occurrences)

@@ -173,13 +229,28 @@ export const getAllByProps = (instance: ReactTestInstance) =>
return results;
};

export const getAllByTestId = (instance: ReactTestInstance) =>
function getAllByTestIdFn(testID: string) {
const results = instance.findAllByProps({ testID });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you noticed in the OP, because findAllByProps returns all components with such ID (so every if the component have a testID prop that is passed further and further, more than one component will be matched).
We want to filter the results to only match the elements which element.type is a string – this is the way we know the renderer rendered the "host node" (in case of RN, it's usually View or Text).

Something like this should be fine:

const results = instance.findAllByProps({ testID })
  .filter(element => typeof element.type === 'string')

FYI, we're not currently doing this filtering for getAllByProps et. al. queries, which is a bug. But we're removing these in v2 anyway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a terrible solution, it won't work if you put testID on a custom component. My solution doesn't have that issue.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, this code:

const { getAllByTestId } = render(
  <TouchableOpacity testID="testID" activeOpacity={0.5} />
)
console.log(getAllByTestId("testID").map(({ props, type }) => ({ props, type })))

Shows this:

      [ { props: { testID: 'testID', activeOpacity: 0.5 },
          type:
           { [Function]
             displayName: 'TouchableOpacity',
             propTypes: [Object],
             getDefaultProps: [Function],
             defaultProps: [Object] } },
        { props:
           { accessible: true,
             accessibilityLabel: undefined,
             accessibilityHint: undefined,
             accessibilityRole: undefined,
             accessibilityStates: undefined,
             style: [Array],
             nativeID: undefined,
             testID: 'testID',
             onLayout: undefined,
             isTVSelectable: true,
             hasTVPreferredFocus: undefined,
             tvParallaxProperties: undefined,
             hitSlop: undefined,
             onStartShouldSetResponder: [Function],
             onResponderTerminationRequest: [Function],
             onResponderGrant: [Function],
             onResponderMove: [Function],
             onResponderRelease: [Function],
             onResponderTerminate: [Function],
             children: [Array] },
          type:
           { [Function: AnimatedComponent]
             __skipSetNativeProps_FOR_TESTS_ONLY: false,
             propTypes: [Object] } },
        { props:
           { accessible: true,
             accessibilityLabel: undefined,
             accessibilityHint: undefined,
             accessibilityRole: undefined,
             accessibilityStates: undefined,
             style: [Object],
             nativeID: undefined,
             testID: 'testID',
             onLayout: undefined,
             isTVSelectable: true,
             hasTVPreferredFocus: undefined,
             tvParallaxProperties: undefined,
             hitSlop: undefined,
             onStartShouldSetResponder: [Function],
             onResponderTerminationRequest: [Function],
             onResponderGrant: [Function],
             onResponderMove: [Function],
             onResponderRelease: [Function],
             onResponderTerminate: [Function],
             children: [Array],
             collapsable: undefined },
          type:
           { [Function: Component]
             displayName: 'View',
             '$$typeof': Symbol(react.forward_ref),
             render: [Function: View] } },
        { props:
           { accessible: true,
             style: [Object],
             testID: 'testID',
             isTVSelectable: true,
             onStartShouldSetResponder: [Function],
             onResponderTerminationRequest: [Function],
             onResponderGrant: [Function],
             onResponderMove: [Function],
             onResponderRelease: [Function],
             onResponderTerminate: [Function],
             children: [Array] },
          type: 'View' } ]

I don't think I need to explain why choosing the last element as the only one that the function should return is a terrible idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind testID is to be applied to the element that directly translates to the native View (or other). If your component is implemented properly and passes props down to children, React should pass them to this very host element ("native" View). Firing events will also work, since they bubble up in the component hierarchy.

If you think it's terrible, feel free to close this PR and create your own fork of the library. I cannot simply accept hacks like generic getBy and getByAll helpers because the point of this library is to be opinionated and prevent you from accessing props wherever possible (that's why we e.g. remove getByProps helpers in v2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants