react-native-testing-library
react-native-testing-library copied to clipboard
Error: Unable to find an element with text: "My text", while that text exist.
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:
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
Yep, easy to reproduce, will try to figure out what happens but that should be fixable on our end.
@AugustinLF easy to reproduce but not easy to fix?
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
@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.
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
@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
@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.
@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
@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.
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.
@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
cc @mdjastrzebski
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.
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.
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?
@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
✕ 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?
@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.
@mdjastrzebski There is an issue on the react repo to export the toJSON method https://github.com/facebook/react/issues/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
@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?
@mdjastrzebski yes I'm very interested ! How to you intend to manage this ?
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.
@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
@pierrezimmermannbam Hmmm that's an interesting development, did you make it pass in RN repo or RNTL repo by switching to jsdom
env?
in RNTL repo, I haven't tried in React repo
@mdjastrzebski nevermind it doesn't work either with jsdom environment, I mixed things up.
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
Maybe this is due to composite vs host element being passed. The RNTL test passed result of 'getByTestId' so a host view.
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
@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