Skip to content

testing library async test isolation

Paulo Gomes da Cruz Junior edited this page Sep 11, 2026 · 1 revision

title: "Fix Guide — Testing Library async test isolation / multiple elements found" type: fix-guide domain: frontend framework: react testing: "vitest + @testing-library/react" severity: error tags: [testing-library, vitest, async-tests, act, tanstack-router, test-isolation] created: 2026-09-11

Fix Guide — Testing Library async test isolation / multiple elements found

Problem

Vitest reports a duplicate-element error in a test that follows a test using an async render helper:

Found multiple elements with the role "form" and name /create reporting unit/i

The failure occurs in ReportingUnitCreate/index.unit.test.tsx, test component initialization > sets up form with initial null values. The component renders one form with aria-label="Create reporting unit" and data-testid="create-ru-form".

The symptom is test contamination: a previous test left a rendered DOM tree in place because its async render helper was not awaited. When the next test queries by role/name, it finds both the leaked DOM and the fresh DOM, triggering the multiple-elements error.

Root Cause

renderComponent creates a TanStack test router and renders inside act(async () => { render(...) }). The preceding test initializes without errors called:

expect(() => renderComponent()).not.toThrow()

This call is synchronous. renderComponent returns a Promise, but the assertion does not await it. The router load and render therefore settle asynchronously after the test completes, leaking the previous render into the shared DOM.

A secondary fragility is the query strategy. Role/name queries are ambiguous when leaked DOM exists. Using a unique test ID removes ambiguity.

Fix

1. Await the async render helper

Make the helper's call site asynchronous and await it before asserting.

// Before
expect(() => renderComponent()).not.toThrow()

// After
await renderComponent()

Ensure every test that invokes a helper performing router loading, async act, or rendering awaits it before the next assertion or test boundary.

2. Use a unique test identifier for the assertion

Prefer getByTestId for a component you control. Add a stable data-testid to the form:

// component
<form aria-label="Create reporting unit" data-testid="create-ru-form" onSubmit={handleSubmit}>

Query the form by test ID:

const form = screen.getByTestId('create-ru-form')

This makes the assertion resilient to name collisions and leaked DOM.

Full Example

import { act, render, screen } from '@testing-library/react'
import { createTestRouter } from '@/config/tests/routerTestHelper'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

async function renderComponent() {
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
  const router = createTestRouter(() => <ReportingUnitCreate />)
  await router.load()
  await act(async () => {
    render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    )
  })
}

describe('ReportingUnitCreate', () => {
  it('initializes without errors', async () => {
    await renderComponent()
    expect(screen.getByTestId('create-ru-form')).toBeTruthy()
  })

  it('sets up form with initial null values', async () => {
    await renderComponent()
    const form = screen.getByTestId('create-ru-form')
    expect(form).toBeTruthy()
  })
})

Prevention

  • Await every helper that performs router loading, async act, or rendering work before asserting or ending a test.
  • Treat Promise-returning render helpers as asynchronous in name, signature, and call sites.
  • Keep cleanup and test isolation assumptions explicit when a helper mounts providers or routers.
  • Prefer a unique, intentional test contract such as data-testid when the assertion targets one known element and accessible-name matching can be ambiguous.

Validation

After the change:

  • npm run test:unit -- --run src/components/waste/ReportingUnits/ReportingUnitCreate/index.unit.test.tsx — 44/44 tests passed
  • npm exec eslint -- --max-warnings=0 src/components/waste/ReportingUnits/ReportingUnitCreate/index.unit.test.tsx — passed
  • npm exec tsc -- --noEmit — passed

No application source or tests outside the changed test file were modified.

Related Pages

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally