React-native-testing-library: Nested custom Text componentns not working correctly

Created on 19 Aug 2020  路  11Comments  路  Source: callstack/react-native-testing-library

Describe the bug

If custom Text components are nested and find with text, then the wrong other element is retrieved.

Steps to Reproduce

const CustomText = ({ children, ...props }: PropsWithChildren<TextProps>) => {
  return <Text {...props}>{children}</Text>;
};
CustomText.displayName = 'CustomText';

const NestedText = () => {
  return (
    <View>
      <CustomText testID="outer">
        <CustomText testID="inner1"></CustomText>
        <CustomText testID="inner2">Hello</CustomText>
      </CustomText>
    </View>
  );
};

it('wrong result', () => {
  const { getByText, debug } = render(<NestedText />);

  const element = getByText(/Hello/);
  debug();

  expect(element.props.testID).toBe('inner2');
});
  console.log
    <View>
      <Text
        testID="outer"
      >
        <Text
          testID="inner1"
        />
        <Text
          testID="inner2"
        >
          Hello
        </Text>
      </Text>
    </View>

      at debugDeep (node_modules/@testing-library/react-native/build/helpers/debugDeep.js:19:13)


Error: expect(received).toBe(expected) // Object.is equality

Expected: "inner2"
Received: "outer"
<Click to see difference>


    at Object.<anonymous> (/Users/mj/Desktop/Turing/MathKing/src/components/shared/AiTeacherCommentText.test.tsx:79:32)
    at Object.asyncJestTest (/Users/mj/Desktop/Turing/MathKing/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37)
    at /Users/mj/Desktop/Turing/MathKing/node_modules/jest-jasmine2/build/queueRunner.js:47:12
    at new Promise (<anonymous>)
    at mapper (/Users/mj/Desktop/Turing/MathKing/node_modules/jest-jasmine2/build/queueRunner.js:30:19)
    at /Users/mj/Desktop/Turing/MathKing/node_modules/jest-jasmine2/build/queueRunner.js:77:41
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

Versions

  npmPackages:
    @testing-library/react-native: 7.0.2 => 7.0.2 
    react: 16.13.1 => 16.13.1 
    react-native: 0.63.2 => 0.63.2 
    react-test-renderer: 16.13.1 => 16.13.1 

bug

All 11 comments

One question more, If I set displayName of CustomText like CustomText.displayName = 'customText'; why debug just show it as <Text> ?

Yup, this is an edge case we don't handle at the moment. Please feel free to send a PR fixing that :)
Related work: https://github.com/callstack/react-native-testing-library/pull/489

One question more, If I set displayName of CustomText like CustomText.displayName = 'customText'; why debug just show it as ?

It's because debug will only show host (native) components and CustomText is not one of these. You can call debug.shallow() using a shallow renderer to display custom component hierarchy to some extent.

Ok I will try Thank you :)

This bug is related to a recursive way of find text node in this library.
I can temporary fix like the following.

const matches = testingLib.getAllByText(text);
const element = matches[matches.length - 1];

Trying to match across nested nodes in an attempt to match what the user sees is an issue for several of our tests too. Most notably when we try to fire an event on an inner Text node.

Right now I can't think of a way to achieve this nested matching and the specific matching behaviour beforehand. Landing #573 would at least give us a way to implement our own overrides, but long term WDYT of adding a matching option to ByText queries - something like a smart mode (for the current behaviour) and a basic/legacy/simple mode? @thymikee

We would have to think about how the exact texting matching option in #554 affects this, if at all.

We could still support nested Texts by traversing _up_ the tree once the original query fail. If the direct parents are Text nodes, we could go up to the first one, construct the string and match using the specified matcher/normalizer. What do you think?

I'm not sure I follow. Is this the kind of algorithm you had in mind?

  1. If current node is a Text node, build string from children (string and number) and run through matcher / normalize. Return node if match.
  2. If parent is a Text node, increment stepUp counter, build string from children of parent (and their children to a depth of stepUp counter) and run through matcher / normalizer. Return if match.
  3. Goto 3 for as long as parent is a Text node and there is no match.

Yea, something like this.

I tried it and found the following problems:

It is somewhat complicated by the native view being wrapped with a React.forwardRef, determining if a parent or child is actually a text element is not cleanly possible.

It matches 2 instances in this case:

<Text>
  <CustomText>Hello</CustomText> <CustomText>World!</CustomText>
</Text>

Reversing the search method like this matches the lowest level element, not its parent, because when searching the instance.find is trying to find elements for which getNodeByText returns true, and using this algorithm it will only return true when evaluating the lowest node.

We only care about host (native) components, with type of "string". This will eliminate any ForwardRefs, Functions etc.

I was rather thinking of building a separate tree of host components while searching for the lowest Text node. If the search fails we have the tree which we can then traverse up for finding the Text that's furthest away from the leaf text node, and then construct the text based on its children.

This sounds like some effort needs to go into this feature. I personally don't need it, what I need is parity with v6 for getByText, which is why I suggested bringing back the simple get text node implementation and having the current smarter implementation behind a flag.

Was this page helpful?
0 / 5 - 0 ratings