skip to content

A React Testing Library test fails with "Unable to find an element…" and there is no browser to look at. What does the library give you to inspect what was actually rendered, and how do you keep that output usable on a large tree?

level: middleimportance: should knowfreq 42%

answer

  1. read the thrown message first
  2. print the body, or just one node
  3. formatted string vs logged output
  4. roles printout for a missed role query
  5. truncation has a limit you can raise

basics

~20 s

Print the rendered DOM: screen.debug() formats the current document body, prettyDOM formats any node you hand it, and logRoles lists the roles in a container. Scope the output to one element and raise the print limit when it truncates.

solid answer

~50 s

The failure message already prints the DOM the query searched, which is often enough — read it before adding anything. Beyond that, `screen.debug()` prints the current `document.body` as formatted HTML, and `screen.debug(element)` narrows it to one node. `prettyDOM(node)` returns the same formatted string instead of logging it, so you can drop it into a custom message. `logRoles(container)` prints the roles present in the tree with the elements under each, which is the fastest way to see why a role-based query missed. The practical trap is truncation: the output is cut at a default length and you end up staring at half a page. Fix it by debugging a scoped element rather than the whole body, or by raising the limit (the `DEBUG_PRINT_LIMIT` environment variable, or the `maxLength` argument). And remember what these show: the DOM, not React internals. If the markup looks wrong, the question is what the component rendered — not what the query did.

go deeper

for a junior

Know that screen.debug() prints the current DOM as formatted HTML and that the query's own failure message already contains that markup — read it before adding anything.

for a middle

Explain the toolkit and when each applies: debug for a snapshot, prettyDOM for a formatted string you place yourself, logRoles when a role-based query missed, plus scoping and the print limit for large trees.

for a senior

Show a debugging method rather than a tool list — print before and after the action, scope to the subtree that should have changed, and interpret an empty body as a failed mount rather than a bad query.

for a principal

Set the expectation that these are investigation aids, not test content: keep them out of committed suites, and invest in assertion messages and harness ergonomics so failures explain themselves without a manual dump.

## Start with the error you already have When a `getBy*` query finds nothing it throws, and the message includes the formatted DOM of the element it searched — normally the whole body. That printout answers most of these failures on its own: the element is missing, or its text differs, or it is inside a portal you did not expect. Reaching for extra tooling before reading the error is the common time-waster. Three things the message tells you at a glance: - **Nothing rendered at all** — the mount failed, likely a missing provider or a throw swallowed into an error boundary. - **A loading state rendered** — the component is still in its pending branch; that is a timing question, not a query question. - **Something rendered but different** — the markup is there with other text, other attributes, or a different structure than you assumed. ## The inspection tools **`screen.debug()`** prints `document.body` as formatted, indented HTML to the console. Call it anywhere in a test to see the state of the DOM at that moment — before an interaction and after it is a cheap way to see what changed. **`screen.debug(element)`** takes a node and prints only that subtree. This is the one to reach for on a real page; whole-body output on a dashboard is unreadable. **`prettyDOM(node)`** returns the formatted string rather than logging it. Useful when you want the markup inside a custom assertion message, or want to log it with your own label: ```js console.log('after submit:', prettyDOM(container)) ``` **`logRoles(container)`** prints each role found in the container together with the elements that have it and their accessible names. When a role-based query fails, this immediately shows whether the element is exposed with a different role than you assumed, or exposed with a name you did not expect. (What each role *means* is accessibility knowledge; here it is purely a diagnostic printout of what the tree currently exposes.) ## Keeping the output readable The formatter truncates long output and prints a note that it did — the default limit is a few thousand characters, which a real app tree blows past instantly. Two responses: - **Scope it.** Debug a container, a section, a row: `screen.debug(screen.getByRole('table'))`. Almost always better than printing everything. - **Raise the limit** when you genuinely need the whole thing: set the `DEBUG_PRINT_LIMIT` environment variable for the run, or pass the `maxLength` argument to `debug`/`prettyDOM`. A third, underused move: print at more than one moment. A single snapshot at the point of failure tells you the end state; a snapshot before and after the action tells you whether the action did anything at all. ```js render(<Filters />) screen.debug() // initial markup // ... interact ... screen.debug(screen.getByRole('list')) // just the part that should have changed ``` ## What these do not show They print DOM. They do not print the React element tree, component names, props, state or hook values. That boundary is not a limitation to work around — it is the point. A component test asserts on what the user-visible output is, so the debugging surface is the same output. If you find yourself wishing you could inspect internal state to explain a failure, the more useful question is usually why the state is not visible in the rendered result. One related habit: these calls are debugging aids, not test content. Leaving `screen.debug()` in a committed test floods CI logs and slows runs on large trees. Some teams lint against it for exactly that reason. ## The transferable principle Every component-test harness needs an answer to "show me what the harness actually saw". Whatever library you use, know how to dump the rendered output, how to scope that dump, and how to raise its limits — and read the failure message first, because a good harness has usually already printed the answer.

  • The debug output is cut off mid-tree. What do you do about it?
    Either scope the print to a smaller node — pass the element you care about to `debug` instead of dumping the whole body — or raise the truncation limit via the `DEBUG_PRINT_LIMIT` environment variable or the `maxLength` argument. Scoping is usually the better instinct, since a full-page dump is rarely readable even when complete.
  • When is logRoles more useful than screen.debug?
    When a role-based query fails and the markup looks superficially right. `debug` shows tags and attributes; `logRoles` shows what the tree actually exposes as roles and names, so you immediately see that the element is exposed differently than you assumed, or carries an unexpected name. It answers the specific question the failing query raised.
  • Why shouldn't debug calls stay in committed tests?
    They are pure noise in CI: every run prints large DOM dumps that bury real failures and cost time on big trees. They also imply an investigation that has finished. Strip them once the bug is understood — and if a failure is genuinely hard to read without them, improve the assertion message instead.

saying these in an interview costs you the question

  • Expects debug to print props, state or component names
  • Dumps the whole body on a large page and gives up
  • Ignores the DOM already printed in the failure message
  • Treats truncated output as the complete tree
  • Leaves debug calls in committed tests

context