react-testing-library
react-testing-library copied to clipboard
findByRole doesn't find the role
-
@testing-library/react
version: 14.1.0 - Testing Framework and version:
- Jest: 29.5.0
- DOM Environment:
Relevant code or config:
https://github.com/perses/perses/blob/nexucis/update-dev-deps/ui/plugin-system/src/components/PluginKindSelect/PluginKindSelect.test.tsx#L54-L55
What you did:
I just upgraded the testing-library from the version 13 to 14 and somehow the function findByRole
doesn't find my component but in the error console it shows it.
I tried to increase the timeout, I also tried a regexp for the name. It doesn't work. I have also taken a look at https://github.com/testing-library/react-testing-library/issues/835 but I'm not using a fancy role here. So I guess my issue is somewhere else.
As a side note, the role attributed previously was button
and now it is combobox
. I don't know if it's really important to notice it.
What happened:
Here the result shown in the console once the test is crashing.
Unable to find role="combobox" and name "Ernie Variable 1"
Ignored nodes: comments, script, style
<body
style=""
>
<div>
<div
class="MuiFormControl-root MuiTextField-root css-1u3bzj6-MuiFormControl-root-MuiTextField-root"
data-testid="plugin-kind-select"
>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary MuiInputBase-formControl css-9ddj71-MuiInputBase-root-MuiOutlinedInput-root"
>
<div
aria-controls=":r3:"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby=":r2:"
class="MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input css-11u53oe-MuiSelect-select-MuiInputBase-input-MuiOutlinedInput-input"
id=":r2:"
role="combobox"
tabindex="0"
>
Ernie Variable 1
</div>
<input
aria-hidden="true"
aria-invalid="false"
class="MuiSelect-nativeInput css-yf8vq0-MuiSelect-nativeInput"
tabindex="-1"
value="ErnieVariable1"
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiSelect-icon MuiSelect-iconOutlined css-hfutr2-MuiSvgIcon-root-MuiSelect-icon"
data-testid="ArrowDropDownIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="MuiOutlinedInput-notchedOutline css-1d3z3hw-MuiOutlinedInput-notchedOutline"
>
<legend
class="css-ihdtdm"
>
<span
class="notranslate"
>
</span>
</legend>
</fieldset>
</div>
</div>
</div>
</body>
But we clearly see that the component with the role combobox
and the name Ernie Variable 1
is there. I know the component is loaded asynchronously, so this combobox with the accurate name is not present immediately but even after increasing the timeout it is still crashing.
So I'm not sure what is wrong in my component or even in my usage of the testing-library.
Experiencing similar thing with findByTestId
after upgrading @testing-library/dom
from version 9.3.1 to 9.3.2 or greater.
I see that you don't seem to use @testing-library/dom
explicitly in your package.json but it's a dependency of @testing-library/react
so what version of @testing-library/dom
are linked in your solution?
I'm thinking about filing an issue on @testing-library/dom
for this but it would be good to know if it works for you if forcing version 9.3.1 of @testing-library/dom
by either adding a resolutions/override field to your package json like below:
"resolutions": {
"@testing-library/dom": "9.3.1",
},
"overrides": {
"@testing-library/dom": "9.3.1",
},
Update: Filed an issue on @testing-library/dom that might be related: https://github.com/testing-library/dom-testing-library/issues/1276
It seems I'm using the latest version of @testing-library/dom
:
npm ls @testing-library/dom
[email protected] /Users/ahusson/workspace/go/src/perses/perses/ui
├─┬ @perses-dev/[email protected] -> ./storybook
│ └─┬ @storybook/[email protected]
│ └── @testing-library/[email protected] deduped
├─┬ @testing-library/[email protected]
│ └── @testing-library/[email protected]
└─┬ @testing-library/[email protected]
└── @testing-library/[email protected]
I tried using what you suggested to override the version and now I'm using the version 9.3.1.
npm ls @testing-library/dom
[email protected] /Users/ahusson/workspace/go/src/perses/perses/ui
├─┬ @perses-dev/[email protected] -> ./storybook
│ └─┬ @storybook/[email protected]
│ └── @testing-library/[email protected] deduped
├─┬ @testing-library/[email protected]
│ └── @testing-library/[email protected] overridden
└─┬ @testing-library/[email protected]
└── @testing-library/[email protected]
But unfortunately I'm experiencing the same issue :(.
/[email protected] └── @testing-library/[email protected]
OK, it might be unrelated to the issue I was seeing with @testing-library/dom
then, it just sounded so similar 😢
I'm experiencing the same issue with a react-native application. Weirdly though, using the new API with role="button"
doesn't work but using the older react-native API with accessibilityRole="button"
does work fine.
I looked in to this a little bit and found it has to do with not getting the accessible name for the element, seemingly due to an issue with handling of the aria-labelledby
attribute.
Failing case
When the aria-labelledby
attribute value is the id
of the same element (referring to itself) then React Testing Library fails to find it.
As in the example given by @Nexucis... the element has aria-labelledby=":r2:"
and id=":r2:"
.
<div
aria-controls=":r3:"
aria-expanded="false"
aria-labelledby=":r2:"
id=":r2:"
role="combobox"
>
Ernie Variable 1
</div>
Example test code added to plugin-system\src\components\PluginKindSelect\PluginKindSelect.test.tsx
, which FAILS
(I ran npm run test -- -- -t PluginKindSelect
)
it('shows the correct selected value', async () => {
render(
<div>
<div aria-controls="controlsEl" aria-expanded="false" aria-labelledby="myDiv" id="myDiv" role="combobox">
Ernie Variable 1
</div>
</div>
);
// Use findByRole to wait for loading to finish and selected value to appear
screen.debug();
const select = await screen.findByRole('combobox', { name: 'Ernie Variable 1' });
expect(select).toBeInTheDocument();
});
Test output
@perses-dev/plugin-system:test: FAIL src/components/PluginKindSelect/PluginKindSelect.test.tsx (7.981 s)
@perses-dev/plugin-system:test: ● Console
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: console.log
@perses-dev/plugin-system:test: <body
@perses-dev/plugin-system:test: style=""
@perses-dev/plugin-system:test: >
@perses-dev/plugin-system:test: <div>
@perses-dev/plugin-system:test: <div>
@perses-dev/plugin-system:test: <div
@perses-dev/plugin-system:test: aria-controls="controlsEl"
@perses-dev/plugin-system:test: aria-expanded="false"
@perses-dev/plugin-system:test: aria-labelledby="myDiv"
@perses-dev/plugin-system:test: id="myDiv"
@perses-dev/plugin-system:test: role="combobox"
@perses-dev/plugin-system:test: >
@perses-dev/plugin-system:test: Ernie Variable 1
@perses-dev/plugin-system:test: </div>
@perses-dev/plugin-system:test: </div>
@perses-dev/plugin-system:test: </div>
@perses-dev/plugin-system:test: </body>
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: at logDOM (../node_modules/@testing-library/dom/dist/pretty-dom.js:87:13)
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: ● PluginKindSelect › shows the correct selected value
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: Unable to find role="combobox" and name "Ernie Variable 1"
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: Ignored nodes: comments, script, style
@perses-dev/plugin-system:test: <body
@perses-dev/plugin-system:test: style=""
@perses-dev/plugin-system:test: >
@perses-dev/plugin-system:test: <div>
@perses-dev/plugin-system:test: <div>
@perses-dev/plugin-system:test: <div
@perses-dev/plugin-system:test: aria-controls="controlsEl"
@perses-dev/plugin-system:test: aria-expanded="false"
@perses-dev/plugin-system:test: aria-labelledby="myDiv"
@perses-dev/plugin-system:test: id="myDiv"
@perses-dev/plugin-system:test: role="combobox"
@perses-dev/plugin-system:test: >
@perses-dev/plugin-system:test: Ernie Variable 1
@perses-dev/plugin-system:test: </div>
@perses-dev/plugin-system:test: </div>
@perses-dev/plugin-system:test: </div>
@perses-dev/plugin-system:test: </body>
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: 75 | // Use findByRole to wait for loading to finish and selected value to appear
@perses-dev/plugin-system:test: 76 | screen.debug();
@perses-dev/plugin-system:test: > 77 | const select = await screen.findByRole('combobox', { name: 'Ernie Variable 1' });
@perses-dev/plugin-system:test: | ^
@perses-dev/plugin-system:test: 78 | expect(select).toBeInTheDocument();
@perses-dev/plugin-system:test: 79 | });
@perses-dev/plugin-system:test: 80 |
@perses-dev/plugin-system:test:
@perses-dev/plugin-system:test: at waitForWrapper (../node_modules/@testing-library/dom/dist/wait-for.js:162:27)
@perses-dev/plugin-system:test: at ../node_modules/@testing-library/dom/dist/query-helpers.js:86:33
@perses-dev/plugin-system:test: at Object.findByRole (src/components/PluginKindSelect/PluginKindSelect.test.tsx:77:33)
Passing case
When the aria-labelledby
points to a separate element (with the name as the content), it will find the accessible name.
For example
<div
aria-controls=":r3:"
aria-expanded="false"
aria-labelledby="anotherElement"
id=":r2:"
role="combobox"
></div>
<span id="anotherElement">Ernie Variable 1</span>
Example test code added to plugin-system\src\components\PluginKindSelect\PluginKindSelect.test.tsx
, which PASSES
(I ran npm run test -- -- -t PluginKindSelect
)
it('shows the correct selected value', async () => {
render(
<div>
<div
aria-controls="controlsEl"
aria-expanded="false"
aria-labelledby="myLabel"
id="myDiv"
role="combobox"
></div>
<span id="myLabel">Ernie Variable 1</span>
</div>
);
// Use findByRole to wait for loading to finish and selected value to appear
const select = await screen.findByRole('combobox', { name: 'Ernie Variable 1' });
expect(select).toBeInTheDocument();
});
Alternatively setting aria-label
(with no aria-labelledby) or wrapping it in a label element will also allow the element to be found.
In this particular case I also found setting the following got the test to pass by wrapping it in a label element.
renderComponent({
pluginType: 'Variable',
value: 'ErnieVariable1',
label: 'Ernie Variable 1',
});
Note this resulted in the combobox div having aria-labelledby=":r2:-label :r2:"
and assumes the :r2:
element is empty.
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby#benefits_and_drawbacks
I dug in to this further and created several test cases below for @testing-library/dom to /src/__tests__/element-queries.js
Interestingly it was possible to get the element by labelText and then filter it to the role using the matching selector.
e.g. getAllByLabelText('myLabel', { selector: '[role="myRole"]' })
However, attempts to get by role with name failed with an error
e.g. getAllByRole('myRole', { name: 'myLabel' })
TypeError: root.getElementById is not a function 271 | 272 | return matches( > 273 | computeAccessibleName(element, { | ^ 274 | computedStyleSupportsPseudoElements: 275 | getConfig().computedStyleSupportsPseudoElements, 276 | }),
which points to https://github.com/eps1lon/dom-accessibility-api/blob/0f33d2d73af4bfcf7f3bc53e13e05dedd07ebc77/sources/util.ts#L101
Tests
test('can get elements labelled with aria-labelledby attribute referencing themselves', () => {
const {getAllByLabelText} = render(`
<div>
<div id="cat" aria-labelledby="cat">Abyssinian</div>
</div>
`)
const result = getAllByLabelText('Abyssinian')
expect(result).toHaveLength(1)
expect(result[0].id).toBe('cat')
})
test('can get elements with matching selector, labelled with aria-labelledby attribute referencing themselves', () => {
const {getAllByLabelText} = render(`
<div>
<div id="cat" aria-labelledby="cat" role="term">Tonkinese</div>
</div>
`)
const result = getAllByLabelText('Tonkinese', { selector: '[role="term"]' })
expect(result).toHaveLength(1)
expect(result[0].id).toBe('cat')
})
test('can get sibling elements with matching selector, labelled with aria-labelledby attribute', () => {
const {getAllByLabelText} = render(`
<div>
<div id="cat" aria-labelledby="cat-label" role="term"></div>
<span id="cat-label">Toyger</span>
</div>
`)
const result = getAllByLabelText('Toyger', { selector: '[role="term"]' })
expect(result).toHaveLength(1)
expect(result[0].id).toBe('cat')
})
// Fails: TypeError: root.getElementById is not a function
test('can get elements by role labelled with aria-labelledby attribute referencing themselves', () => {
const {getAllByRole} = render(`
<div>
<div id="cat" aria-labelledby="cat" role="term">Birman</div>
</div>
`)
const result = getAllByRole('term', { name: 'Birman' })
expect(result).toHaveLength(1)
expect(result[0].id).toBe('cat')
})
// Fails: TypeError: root.getElementById is not a function
test('can get sibling elements by role labelled with aria-labelledby attribute', () => {
const {getAllByRole} = render(`
<div>
<div id="cat" aria-labelledby="cat-label" role="term"></div>
<span id="cat-label">Siamese</span>
</div>
`)
const result = getAllByRole('term', { name: 'Siamese' })
expect(result).toHaveLength(1)
expect(result[0].id).toBe('cat')
})
Test Output
Summary of all failing tests
FAIL src/__tests__/element-queries.js (5.413 s)
● can get elements by role labelled with aria-labelledby attribute referencing themselves
TypeError: root.getElementById is not a function
271 |
272 | return matches(
> 273 | computeAccessibleName(element, {
| ^
274 | computedStyleSupportsPseudoElements:
275 | getConfig().computedStyleSupportsPseudoElements,
276 | }),
at getElementById (node_modules/dom-accessibility-api/sources/util.ts:99:22)
at Array.map (<anonymous>)
at map (node_modules/dom-accessibility-api/sources/util.ts:99:5)
at computeTextAlternative (node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:584:18)
at computeTextAlternative (node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:721:3)
at computeAccessibleName (node_modules/dom-accessibility-api/sources/accessible-name.ts:40:31)
at src/queries/role.ts:273:30
at Array.filter (<anonymous>)
at filter (src/queries/role.ts:266:6)
at allQuery (src/query-helpers.ts:106:17)
at query (src/query-helpers.ts:177:17)
at Object.getAllByRole (src/__tests__/element-queries.js:307:18)
● can get sibling elements by role labelled with aria-labelledby attribute
TypeError: root.getElementById is not a function
271 |
272 | return matches(
> 273 | computeAccessibleName(element, {
| ^
274 | computedStyleSupportsPseudoElements:
275 | getConfig().computedStyleSupportsPseudoElements,
276 | }),
at getElementById (node_modules/dom-accessibility-api/sources/util.ts:99:22)
at Array.map (<anonymous>)
at map (node_modules/dom-accessibility-api/sources/util.ts:99:5)
at computeTextAlternative (node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:584:18)
at computeTextAlternative (node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:721:3)
at computeAccessibleName (node_modules/dom-accessibility-api/sources/accessible-name.ts:40:31)
at src/queries/role.ts:273:30
at Array.filter (<anonymous>)
at filter (src/queries/role.ts:266:6)
at allQuery (src/query-helpers.ts:106:17)
at query (src/query-helpers.ts:177:17)
at Object.getAllByRole (src/__tests__/element-queries.js:320:18)
amazing investigation @agentdylan !! Thank you so much for taking time to find a workaround. Hope it will help to fix the issue if there is a fix to do
Happy to help! yeah, not sure what the complete fix is exactly though and suspect the issue/fix may be in another dependency. Hopefully my investigation is useful and perhaps someone with more familiarity in this area will be able to figure it out 😄
I believe I've traced it to the root cause of the issue and filed an issue on the dependency. https://github.com/eps1lon/dom-accessibility-api/issues/1018
It is unique to the combobox
role when using aria-labelledby
to point to itself as in your example.
It is interesting there was also issues with my tests using the role term
however, so there may be separate or additional fixes to be done.
I dug in to this further and created several test cases below for @testing-library/dom to
/src/__tests__/element-queries.js
Interestingly it was possible to get the element by labelText and then filter it to the role using the matching selector. e.g.
getAllByLabelText('myLabel', { selector: '[role="myRole"]' })
However, attempts to get by role with name failed with an error e.g.
getAllByRole('myRole', { name: 'myLabel' })
TypeError: root.getElementById is not a function 271 | 272 | return matches( > 273 | computeAccessibleName(element, { | ^ 274 | computedStyleSupportsPseudoElements: 275 | getConfig().computedStyleSupportsPseudoElements, 276 | }),
which points to https://github.com/eps1lon/dom-accessibility-api/blob/0f33d2d73af4bfcf7f3bc53e13e05dedd07ebc77/sources/util.ts#L101
Tests Test Output
Thanks @agentdylan the findByLabelText
solution worked perfectly for our use case with @Nexucis.
We moved to const select = await screen.findByLabelText('Ernie Variable 1', { selector: '[role="combobox"]' });
That said, this doesn't fix the issue, so I believe this should be kept open