Fix: React Testing Library Not Finding Element — Unable to Find Role or Text
Quick Answer
How to fix React Testing Library query failures — getByRole vs getByText, async queries, accessible names, waitFor patterns, custom queries, and common selector mistakes.
The Problem
React Testing Library throws an error when querying an element:
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name "Submit"
Here are the accessible roles:
button:
Name "Cancel":
<button>Cancel</button>
button:
Name "submit form":
<button aria-label="submit form">...</button>Or getByText can’t find visible text:
TestingLibraryElementError: Unable to find an element with the text: /submit/iOr a query fails because the element renders asynchronously:
TestingLibraryElementError: Unable to find an element by: [data-testid="user-list"]Or waitFor or findBy never resolves.
Why This Happens
React Testing Library (RTL) queries the DOM using accessibility semantics — roles, labels, and visible text — not CSS selectors or component internals. Common failure causes:
- Wrong accessible name —
getByRole('button', { name: 'Submit' })matches the button’s accessible name, which comes from its text content,aria-label, oraria-labelledby. A mismatch (e.g., the button says “submit form” not “Submit”) causes the query to fail. - Element not yet rendered —
getBy*queries are synchronous. If the element appears after an async operation (fetch, state update), usefindBy*(async) orwaitFor. - Wrong role — HTML elements have implicit ARIA roles. An
<a>withouthrefis a generic role, not a link. A<div>withonClickis not a button role. - Hidden elements — RTL skips elements with
aria-hidden,display: none, orvisibility: hiddenby default. Elements hidden from the accessibility tree won’t be found. - Text normalization — RTL normalizes whitespace by default. But if the text contains special characters, is split across elements, or has extra whitespace, exact matching fails.
Fix 1: Use the Right Query for the Job
RTL provides multiple query types. Use them in priority order:
// Priority 1 — getByRole (most semantic, preferred)
// Matches by ARIA role + accessible name
getByRole('button', { name: /submit/i })
getByRole('textbox', { name: 'Email' })
getByRole('checkbox', { name: 'Remember me' })
getByRole('heading', { name: 'Sign In', level: 2 })
getByRole('link', { name: 'Learn more' })
getByRole('img', { name: 'Profile photo' })
// Priority 2 — getByLabelText (for form elements)
getByLabelText('Email address')
getByLabelText(/password/i)
// Priority 3 — getByPlaceholderText (fallback for forms)
getByPlaceholderText('Enter your email')
// Priority 4 — getByText (for non-interactive elements)
getByText('Welcome back')
getByText(/loading/i)
// Priority 5 — getByDisplayValue (for current value of inputs)
getByDisplayValue('[email protected]')
// Priority 6 — getByAltText (for images)
getByAltText('Company logo')
// Priority 7 — getByTitle (for title attributes)
getByTitle('Close dialog')
// Last resort — getByTestId (avoid if possible)
// Requires: <div data-testid="user-profile">
getByTestId('user-profile')Fix 2: Understand Accessible Names
getByRole with name matches the element’s accessible name, not its text content alone:
// Component
function LoginForm() {
return (
<form>
<button>Submit</button> {/* accessible name: "Submit" */}
<button aria-label="submit form">→</button> {/* accessible name: "submit form" */}
<button aria-labelledby="btn-label">
<span id="btn-label">Confirm action</span>
<span>→</span>
</button> {/* accessible name: "Confirm action" */}
</form>
);
}
// Tests
getByRole('button', { name: 'Submit' }) // ✓ matches first button
getByRole('button', { name: /submit/i }) // ✓ matches both first and second (case-insensitive regex)
getByRole('button', { name: 'submit form' }) // ✓ matches second button
getByRole('button', { name: 'Confirm action' }) // ✓ matches third button
getByRole('button', { name: '→' }) // ✗ '→' is not the accessible name of button 2Debug accessible names:
import { render, screen } from '@testing-library/react';
import { logRoles } from '@testing-library/dom';
test('debug accessible roles', () => {
const { container } = render(<LoginForm />);
// Print all roles and accessible names in the rendered output
logRoles(container);
// Output:
// button:
// Name "Submit":
// <button>Submit</button>
// button:
// Name "submit form":
// <button aria-label="submit form">→</button>
});Fix 3: Use findBy* for Async Elements
getBy* is synchronous — it fails immediately if the element isn’t present. Use findBy* for elements that appear asynchronously:
// Component that fetches data
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
// WRONG — getBy fails because list is empty on initial render
test('shows users', () => {
render(<UserList />);
const items = screen.getByRole('listitem'); // Fails: no listitems yet
});
// CORRECT — findBy waits for the element to appear
test('shows users', async () => {
render(<UserList />);
// findByRole waits up to 1000ms for the element to appear
const firstItem = await screen.findByRole('listitem');
expect(firstItem).toBeInTheDocument();
});
// CORRECT — waitFor for more complex conditions
test('shows all users', async () => {
render(<UserList />);
await waitFor(() => {
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(3);
});
});findBy* vs waitFor:
// findBy* = getBy* + waitFor — use for finding a single element
const button = await screen.findByRole('button', { name: 'Submit' });
// waitFor = retry the assertion until it passes — use for complex conditions
await waitFor(() => {
expect(screen.getAllByRole('listitem')).toHaveLength(5);
});
// waitForElementToBeRemoved — wait for loading state to disappear
await waitForElementToBeRemoved(() => screen.getByText('Loading...'));
// Or
await waitForElementToBeRemoved(screen.getByText('Loading...'));Fix 4: Mock API Calls Correctly
RTL tests run in jsdom, not a real browser. Fetch calls need to be mocked:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// Using MSW (Mock Service Worker) — recommended
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('loads and displays users', async () => {
render(<UserList />);
// Wait for loading to finish
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
// Now assert on the loaded state
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});Simple fetch mock with jest.fn():
// Mock global fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: 1, name: 'Alice' }]),
} as Response)
);
afterEach(() => {
jest.restoreAllMocks();
});Fix 5: Test User Interactions
Use @testing-library/user-event for realistic user interactions:
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
test('submits the form', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Set up user event (v14+ requires setup)
const user = userEvent.setup();
// Type into input
await user.type(screen.getByLabelText('Email'), '[email protected]');
await user.type(screen.getByLabelText('Password'), 'password123');
// Click the submit button
await user.click(screen.getByRole('button', { name: 'Sign In' }));
// Assert
expect(handleSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
});
});
test('shows validation error on empty submit', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(await screen.findByText('Email is required')).toBeInTheDocument();
});Fix 6: Handle Context Providers
Components that use context (Redux, Theme, Router) need their providers in tests:
// Custom render function with providers
import { render, RenderOptions } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from './ThemeContext';
function AllProviders({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, // Disable retries in tests
});
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<ThemeProvider>
{children}
</ThemeProvider>
</MemoryRouter>
</QueryClientProvider>
);
}
const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
render(ui, { wrapper: AllProviders, ...options });
// Export customRender as 'render' — drop-in replacement
export { customRender as render };
export * from '@testing-library/react';// Test — uses custom render with all providers
import { render, screen } from './test-utils'; // Custom render
test('shows user profile', async () => {
render(<UserProfile userId={1} />);
expect(await screen.findByRole('heading', { name: 'Alice' })).toBeInTheDocument();
});Fix 7: Debug Failing Queries
When a query fails unexpectedly:
test('debug failing query', () => {
render(<MyComponent />);
// Print the full rendered DOM
screen.debug();
// Or debug a specific element:
screen.debug(screen.getByRole('main'));
// Print all accessible roles
import { logRoles } from '@testing-library/dom';
const { container } = render(<MyComponent />);
logRoles(container);
// Check if an element exists without throwing
const button = screen.queryByRole('button', { name: 'Submit' });
console.log('Button found:', button);
// null = not found, HTMLElement = found
// See what text is in the document
console.log(document.body.innerHTML);
});Use prettyDOM for formatted output:
import { prettyDOM } from '@testing-library/dom';
test('inspect element', async () => {
render(<UserCard userId={1} />);
const card = await screen.findByRole('article');
console.log(prettyDOM(card));
// Prints formatted HTML of the element
});Still Not Working?
within for scoping queries — if multiple elements have the same role/name, scope the query to a specific container:
import { within } from '@testing-library/react';
const nav = screen.getByRole('navigation');
const links = within(nav).getAllByRole('link');
// Only finds links inside the nav elementMatching partial text — use a regex or { exact: false }:
getByText('Submit') // Exact match
getByText(/submit/i) // Regex — case-insensitive
getByText('Submit', { exact: false }) // Substring matchact() warnings — if you see “Warning: An update to X inside a test was not wrapped in act(…)”, wrap state updates in act() or use waitFor which wraps automatically:
// RTL's userEvent and findBy/waitFor wrap in act() automatically
// Only manually use act() for direct state updates:
act(() => {
store.dispatch(someAction());
});For related React testing issues, see Fix: Jest Async Test Timeout and Fix: Jest Module Mock Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: React Three Fiber Not Working — Canvas Blank, Models Not Loading, or Performance Dropping
How to fix React Three Fiber (R3F) issues — Canvas setup, loading 3D models with useGLTF, lighting, camera controls, animations with useFrame, post-processing, and Next.js integration.