Skip to content
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

Error: Unable to find an element with text: "My text", while that text exist. #937

Closed
retyui opened this issue Mar 18, 2022 · 36 comments
Closed
Labels
bug Something isn't working

Comments

@retyui
Copy link
Contributor

retyui commented Mar 18, 2022

Describe the bug

getByText can find element if text render component with text as not children (See "Steps to Reproduce" below)

-const Trans = p => p.i18nKey; // can find (error)
+const Trans = p => p.children; // everything ok

Expected behavior

I see in debug output (see screenshot below) that text exists! But API can find it(
It should work properly!

Steps to Reproduce

    test('test',  () => {
      const { Text } = require('react-native');
      const { render } = require('@testing-library/react-native');
      const Trans = p => p.i18nKey;

      const screen = render(
        <Text>
          <Trans i18nKey="My text" />
        </Text>,
      );

      screen.debug();

      expect(screen.getByText('My text')); // Error: Unable to find an element with text: My text
    });

Screenshots

Output:

image

Versions

  npmPackages:
    @testing-library/react-native: ^7.2.0 => 7.2.0 
    react: 17.0.2 => 16.13.1 
    react-native: 0.66.4 => 0.66.4 
    react-test-renderer: 17.0.2 => 17.0.2 
@AugustinLF
Copy link
Collaborator

Yep, easy to reproduce, will try to figure out what happens but that should be fixable on our end.

@retyui
Copy link
Contributor Author

retyui commented Mar 22, 2022

@AugustinLF easy to reproduce but not easy to fix?

@AugustinLF
Copy link
Collaborator

I (or whoever's interested in) just need to take the time, but I don't think it should be very hard. I'll try to do that later this week, or next week

@Norfeldt
Copy link

@AugustinLF I know that this is a feature request. But is is possible to have the .debug() run automatically when a getBy* matcher fails? Recall react-testing-library had that and it was really useful to see how the component looks when debugging why it can't get a hold of it.

@pierrezimmermannbam
Copy link
Collaborator

Not sure this is easy to fix, the type of <Trans i18nKey="text"/> isn't string so the getChildrenAsText method is looking for strings in its children but it doesn't have any. For this to work it would require to be able to tell that it renders as a string but i'm not sure it's easily doable.

@retyui this can be fixed on your end by mocking the Trans component like this

jest.mock('./Trans', () => {
  const { Text } = require('react-native');

  return {
    Trans: (props) => {
      return <Text>{props.i18nKey}</Text>;
    },
  };
});

However if there is an effective way to fix this it would be very nice, or maybe it could be documented somewhere

@pierrezimmermannbam
Copy link
Collaborator

@AugustinLF I have found a solution but it's quite complex so I'm not sure it's something that should be done.
From what I've seen this can't be fixed if we use the type ReactTestInstance for getByText because there does not seem to be a way to know the component renders as a string, however it is doable by using the ReactTestRendererJSOn type. The thing is we also need to have access to the instance because it is what the query returns in the end. This is achievable by using the tree representation of type ReactTestRendererTree which combines the instance and a more json like modelization. This would however mean significant changes as it would almost completely change the way queries are done. I've been working on a draft pr and I think it would work and this type seems more appropriate for queries but I wanted to have your opinion first before spending more time on this

@AugustinLF
Copy link
Collaborator

@pierrezimmermannbam that sounds like an interesting approach. Working with a "simpler" representation, based only on the platform components also feels more testing-library like.

One thing we'll need to cater for is that people do rely on firing fake events for some events (see #918 , I've also had to do that for momentum events on FlatList) . Some scroll events, swipes, etc. don't play well with the fact that we're not really rendering the native components (while jsdom does fire the real DOM events).

I'm not sure how that last point would play with this rearchitecture, but I think we should either offer a fallback API (current state) or figure a way to improve the behaviour.

@pierrezimmermannbam
Copy link
Collaborator

@AugustinLF I'm not sure I understood your point about fake events. The changes I suggested would change the way queries are implemented internally but the api would remain the same as they would still return ReactTestInstance type so there shouldn't be any impact on the way fireEvent works. I'm not sure either how the issue if events not being supported could be fixed by using this approach but there are definitely things I'm missing here

@AugustinLF
Copy link
Collaborator

@pierrezimmermannbam the current implementation of fireEvent crawls the tree up until it finds a ReactTestInstance with a prop corresponding to the given event, and fires it. Even if that's not a real event/a custom component.

I'll add tests for those cases in main, this way we can easily make sure there's no regressions and you shouldn't have to worry about that.

Regarding tests, I'll go through the repo and make sure that we have the desired coverage before we merge your PR. Luckily our tests do test external APIs so internal rewrite would be safe, but I'll make sure we're not missing any cases.

@thymikee
Copy link
Member

thymikee commented Mar 29, 2022

Regarding returning ReactTestInstance from queries – we considered replacing it with a QueryResult object or something, that would only give you access to specific fields instead of whole object. This is potentially breaking, but the direction where we'd like to go.

@pierrezimmermannbam
Copy link
Collaborator

@AugustinLF @thymikee I have made some progress on this, I opened a draft pr but I've hit an issue. The queries by text work fine using the json representation but there are some problems.

First, I still need to get the ReactTestInstance from the renderer.root because the instances in the ReactTestRendererTree type are in fact not of type ReactTestInstance and do not have parents which is problematic for the fireEvent. I was able to fix this by finding the ReactTestInstance matching the result of the search though I'm not entirely sure it is reliable.

The second issue is with within. For within to work, the queries return type and what is used to build the api need to be of the same type and I broke that by building queries with a ReactTestRenderer type and returning the same type as before. It would be doable to have the following type :

type QueryResult  = ReactTestInstance & { tree : ReactTestRendererTree }

However this would require to change not only the byText queries but all of them, which is more complex but shouldn't be too much of an issue if well refactored.
It would change the type of the api but not cause a breaking change as we'd be only adding a new field so it shouldn't be too problematic.

So before going further I wanted to have your inputs on this and make sure the implementation I started was what you also had in mind

@thymikee
Copy link
Member

thymikee commented May 4, 2022

cc @mdjastrzebski

@mdjastrzebski
Copy link
Member

mdjastrzebski commented May 4, 2022

TLDR

string text output (not wrapped in Text object) is not traversable using ReactTestInstance.findAll method that all text queries use. However, you can fix the test quite easily by wrapping Trans output in Text instance like this:

const Trans = (p) => <Text>{p.i18nKey}</Text>;

Longer explanation

We we run getByText it internally uses ReactTestInstance.findAll to go through all elements of render tree.

Having this as our render input:

  const Trans = (p) => p.i18nKey;

  const screen = render(
    <Text>
      <Trans i18nKey="My text" />
    </Text>
  );

We get findAll traversing following nodes of ReactTestInstance type:

    NODE - Text composite class component
    NODE - Text host component
    NODE - Our Trans function component

As you can see there is no final "text node" with "My text" content. The leaf component is our Trans component.

From my investigation in what it offers in terms of API, there is no rendered output field available for easy examination. However when I inspect the actual object structure under tests we can see React fiber details:

{
     _fiber: <ref *1> FiberNode {
        tag: 0,
        key: null,
        elementType: [Function: Trans],
        type: [Function: Trans],
        // omitted for brevity
        child: FiberNode {
          tag: 6,
          key: null,
          elementType: null,
          type: null,
          stateNode: [Object],
          return: [Circular *1],
          child: null,
          sibling: null,
          index: 0,
          ref: null,
          pendingProps: 'My text',
          memoizedProps: 'My text',
          // ... omitted for brevity
        },
        sibling: null,
        index: 0,
        ref: null,
        pendingProps: { i18nKey: 'My text' },
        memoizedProps: { i18nKey: 'My text' },
        // ... omitted for brevity
      }
    }

It's pretty lengthy but under _fiber.child we find the actual text content. Otherwise there is not way to access the rendered output.

I would be wary of building our code based on internal React representation and stick to public types only.

@mdjastrzebski
Copy link
Member

Regarding using ReactTestRendererTree/ReactTestRendererJSON as a basis for working with queries that's an interesting question. It seems that there might be some benefit for using these however this would a very deep change for RNTL and should be careful considered whether it is fit for our purpose, also the light of the fact that all our queries return ReactTestInstance and users code is probably relying it, so changing that contract would be a really breaking change.

@mdjastrzebski mdjastrzebski added the bug Something isn't working label May 4, 2022
@mdjastrzebski
Copy link
Member

It would be nice if we could continue using ReactTestInstance but just for text finding purpose we could transition to ReactTestRendererTree/JSON for children of that Text element. While the functionality for that is already in React Test Renderer in form of toJSON function or toTree function however these functions are not exported. With the current API we are limited to calling toJSON/toTree on root renderer element.

Can we somehow make React Test Renderer to export toJSON/toTree free-floating function that we could pass ReactTestInstance.instance object and get ReactTestRendererJSON output?

@pierrezimmermannbam
Copy link
Collaborator

@mdjastrzebski I think it is a very interesting approach, it could event be possible to still use the reactTestInstance.findAll method and then only use the ReactTestRendererJSON type for the getChildrenAsText method which would be a way less significant change. I'll try it and if it works ask on the React test renderer repo whether it is a change they are willing to make

@BartoszKlonowski BartoszKlonowski changed the title Error: Unable to find an element with text: My text, but text exist!!! Error: Unable to find an element with text: "My text", while that text exist. May 5, 2022
@nanda-kumar-k
Copy link

✕ renders learn react link (36 ms)

● renders learn react link

TestingLibraryElementError: Unable to find an element with the text: /learn react/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Ignored nodes: comments, <script />, <style />
<body>
  <div>
    <div>
      <div
        class="sc-bczRLJ jlIjgm"
      >
        navbar
      </div>
      <button
        class="sc-gsnTZi bVnXBy"
      >
        Normal Button
      </button>
    </div>
  </div>
</body>

  4 | test('renders learn react link', () => {
  5 |   render(<App />);
> 6 |   const linkElement = screen.getByText(/learn react/i);
    |                              ^
  7 |   expect(linkElement).toBeInTheDocument();
  8 | });
  9 |

  at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:40:19)
  at node_modules/@testing-library/dom/dist/query-helpers.js:90:38
  at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
  at getByText (node_modules/@testing-library/dom/dist/query-helpers.js:111:19)
  at Object.<anonymous> (src/App.test.js:6:30)
  at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
  at runJest (node_modules/@jest/core/build/runJest.js:404:19)
  at _run10000 (node_modules/@jest/core/build/cli/index.js:320:7)
  at runCLI (node_modules/@jest/core/build/cli/index.js:173:3)

Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 1.499 s
Ran all test suites.
Error: Process completed with exit code 1.

How to resolve this error?

@mdjastrzebski
Copy link
Member

@nanda-kumar-k the posted errors seems to concern React Testing Library, and not React Native Testing Library, as the stack trace mentions @testing-library/dom and you use HTML elements.

@pierrezimmermannbam
Copy link
Collaborator

@mdjastrzebski There is an issue on the react repo to export the toJSON method facebook/react#14539 but it's stale. Also I've looked a bit at react-test-renderer code and the toJSON method does not use the type ReactTestInstance, but something entirely different that's not exported either. And indeed it is probably impossible to go from ReactTestInstance to ReactTestRendererJSON type or else we should be able to fix this issue by using a ReactTestInstance. This means that this issue cannot be solved until the queries are using ReactTestInstance and since we should be able to do queries on elements returned by queries they should also return something else in order for this to be fixed

@mdjastrzebski
Copy link
Member

@pierrezimmermannbam I've recently started working on PR to facebook/react repo to get toJSON exported. I've made some progress but also got stuck on some obstacles and TRT complexity. Would you like to help me with this PR?

@pierrezimmermannbam
Copy link
Collaborator

@mdjastrzebski yes I'm very interested ! How to you intend to manage this ?

@mdjastrzebski
Copy link
Member

mdjastrzebski commented Sep 20, 2022

So I've stated with locally modifying the react-test-renderer package, by exposing toJSON method on ReactTestInstance. You can re-apply this yourself using patch-package: https://gist.github.com/mdjastrzebski/101f6930b27b3f82801d51c4cb31ecb7

That allowed me to write some POC tests in RNTL repo, e.g.:

import * as React from 'react';
import { View, Text } from 'react-native';
import { render, screen } from '../..';

const Trans = () => 'Hello';

test('toJSON of Text with Trans', () => {
  render(
    <View>
      <Text testID="subject">
        <Trans />
      </Text>
    </View>
  );

  const subject = screen.getByTestId('subject');
  expect(subject.toJSON()).toMatchInlineSnapshot(`
    <Text
      testID="subject"
    >
      Hello
    </Text>
  `);
});

So far so good. Next I went to implement this as a PR for facebook/react repo.

Here is the respective branch on my React repo fork: https://github.com/mdjastrzebski/react/tree/feature/test-instance-expose-toJSON

However, when trying to write tests for toJSON() method (also in the forked repo) I've got weird errors, where following code would fail:

// ReactTestRenderer-test.internal.js
describe('ReactTestRenderer', () => {
  // This is original RTR test
  it('renders a simple component', () => {
    function Link() {
      return <a role="link" />;
    }
    const renderer = ReactTestRenderer.create(<Link />);
    expect(renderer.toJSON()).toEqual({
      type: 'a',
      props: {role: 'link'},
      children: null,
    });    
    // Below is my added part that fails
    expect(renderer.root.toJSON()).toEqual({
      type: 'a',
      props: {role: 'link'},
      children: null,
    });
  });
});

In the tests the this._fiber.stateNode is null unlike when using this under RNTL.

@pierrezimmermannbam maybe you can spot some issue with code or tests? I've added you as a collaborator to my forked React repo.

@pierrezimmermannbam
Copy link
Collaborator

@mdjastrzebski The same test also fails in the RNTL repo with the patch-package applied, but that's because the testenvironment is node, I was able to make the test pass by switching the environment to jsdom

@mdjastrzebski
Copy link
Member

@pierrezimmermannbam Hmmm that's an interesting development, did you make it pass in RN repo or RNTL repo by switching to jsdom env?

@pierrezimmermannbam
Copy link
Collaborator

in RNTL repo, I haven't tried in React repo

@pierrezimmermannbam
Copy link
Collaborator

@mdjastrzebski nevermind it doesn't work either with jsdom environment, I mixed things up.

@pierrezimmermannbam
Copy link
Collaborator

It works for the case with the link (not when rendering the view) if toJSON method return toJSON(this._fiber.child.stateNode) instead of toJSON(this._fiber.stateNode) so there seems to be cases where you'd want to use the child or maybe the children if there are several, I have no clue yet what's the logic behind this though

@mdjastrzebski
Copy link
Member

Maybe this is due to composite vs host element being passed. The RNTL test passed result of 'getByTestId' so a host view.

@pierrezimmermannbam
Copy link
Collaborator

It also works when using getByText, but when trying to use toJSON on the container returned from the render function it fails

test('toJSON of Text with Trans', () => {
  const { container } = render(
    <View>
      <Text testID="subject">
        <Trans />
      </Text>
      <Text>hello</Text>
    </View>
  );

  expect(container.toJSON()).toMatchInlineSnapshot();
});

In this case also using this_fiber.child.stateNode works but this time this._fiber_stateNode is not null but it doesn't have a tag property

@pierrezimmermannbam
Copy link
Collaborator

pierrezimmermannbam commented Sep 22, 2022

@mdjastrzebski The following implementation seems to work

toJSON(): ReactTestRendererNode | null {
    if (typeof this._fiber.type === 'string') {
      return toJSON(this._fiber.stateNode);
    }

     return toJSON(this._fiber.child.stateNode);
  }

You were right it does look like it only works on host component

@mdjastrzebski
Copy link
Member

@pierrezimmermannbam I've been able to make some reasonable progress, pls take a look at https://github.com/mdjastrzebski/react/blob/5ec75abd232a2315d90c30d3472ee5ce6130dbcf/packages/react-test-renderer/src/ReactTestRenderer.js#L132

I've created exported toJSON function that essentially calls the original toJSON when it is a host element, otherwise it recursively calls itself on all its children and collects the results.

Proposed implementation passes all RTR internal tests on equality between renderer.toJSON() with toJSON(renderer.root): https://github.com/mdjastrzebski/react/blob/5ec75abd232a2315d90c30d3472ee5ce6130dbcf/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js

@pierrezimmermannbam
Copy link
Collaborator

pierrezimmermannbam commented Sep 26, 2022

@mdjastrzebski awesome! I see you're now exporting a function instead of adding a toJSON method to the ReactTestInstance class, why is that ? Wouldn't it be more convenient to have the method on the class ? It would allow for instance to take snapshots of elements returned by query without having to call the toJSON function and still have the JSON representation

@mdjastrzebski
Copy link
Member

@pierrezimmermannbam I've started with implementing toJSON as a method on ReactTestInstance, however I've encountered two issues that made free function a more natural approach:

  1. testInstanceToJSON is recursive, as it needs to call itself for child nodes, and that requires ability to pass node on which to run the function
  2. testInstanceToJSON accepts either ReactTestInstance | string as sometimes children might be string. Particular there was one existing unit test for renderer.toJSON() method that I could not duplicate as renderer.root.toJSON() because renderer.root was actually a string.

Thinking about this after your comment, I think that point 1 might be solvable by invoking child.toJSON() from testInstance.toJSON() and adding child type checks for string values. I will review my code tomorrow to see which feels like a better API.

@mdjastrzebski
Copy link
Member

mdjastrzebski commented Sep 27, 2022

@pierrezimmermannbam I refactored back into toJSON method on ReactTestInstance and I've submitted PR: facebook/react#25329

Let's see how the review goes...

@pierrezimmermannbam
Copy link
Collaborator

It looks like this issue is fixed! I think this pr #1222 of @mdjastrzebski was the fix because we now don't look only at the direct children, so I guess from the beginning we only needed to look deeper as long as children weren't root elements

@mdjastrzebski
Copy link
Member

@pierrezimmermannbam Indeed. This exact issue seems to be solved now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants