VLearnVibium

How to Test a React App with Vibium

Test a React app with Vibium in Python — wait for components to render, drive state with click() and type(), and assert on the DOM without flaky sleeps.

By Pramod Dutta··4 min read·Verified with Vibium 26.2
▶ Animated overview · made with Remotion

To test a React app with Vibium, launch a browser, navigate to your app, then drive components with find(), click(), and type() while Vibium auto-waits for each element to render. Vibium is AI-native browser automation built on WebDriver BiDi and ships as a single Go binary that auto-downloads Chrome for Testing — pip install vibium and you are ready. Because Vibium polls every element until it is actionable, React's asynchronous rendering no longer flakes your tests: you assert on the element a component paints, not on a guessed timer. The same script works against Create React App, Next.js, Vite, and Remix builds because Vibium tests the final rendered DOM exactly as a user sees it. Created by Jason Huggins, the co-creator of Selenium and Appium, Vibium gives React developers the deterministic findclick → assert loop without driver wrangling or manual sleep() calls.

What is the React test script?

from vibium import browser_sync as browser
 
vibe = browser.launch()
vibe.go("http://localhost:3000")
 
# A React click handler updates state; the new text renders async.
vibe.find('button[data-testid="increment"]').click()
 
# find() waits until React has re-rendered with the new value.
count = vibe.find('[data-testid="count"]')
assert count.text() == "1"
 
vibe.quit()

This clicks a button wired to a React state update, then asserts on the counter the component re-renders. Because find() waits for the element to be actionable, you never race React's render cycle.

How does each step work?

  1. vibe.go(url) — opens your dev server (or a deployed build) and waits for the initial render.
  2. find('button[...]').click() — fires the React event handler that updates component state.
  3. vibe.find('[data-testid="count"]') — waits for and grabs the element React re-renders. Finding it is the wait.
  4. count.text() — reads the rendered text so you can assert on the new state.
  5. vibe.quit() — tears the browser down.

Vibium auto-waits on actionability, so a slow re-render on a busy CI machine still passes — there is no sleep() to tune.

How do I select React components reliably?

React class names are often hashed or generated, so prefer stable hooks. Add data-testid attributes to the components you test and target them with find():

# Stable selector that survives styling and refactors.
vibe.find('[data-testid="login-form"] input[name="email"]').type("user@example.com")
 
# Vibium also finds elements by accessible role or label.
vibe.find(role="button", text="Sign in").click()

The semantic find(role=..., text=...) form matches the accessibility tree, which is ideal for React because it ignores implementation details and tests what users and screen readers actually perceive.

How do I wait for data fetched in useEffect?

When a component fetches data in useEffect and renders it after mount, assert on the element that only appears once the data arrives. Vibium polls until it is actionable, so no sleeps are needed:

from vibium import browser_sync as browser
 
vibe = browser.launch()
vibe.go("http://localhost:3000/users")
 
# Waits until the first row the effect renders is in the DOM.
first = vibe.find('[data-testid="user-row"]')
print(first.text())
 
# Now read the whole list, which is already present.
rows = vibe.findAll('[data-testid="user-row"]')
print(f"Loaded {len(rows)} users")
 
vibe.quit()

findAll() returns immediately with whatever currently matches, so call it only after a find() for one row has already waited the data into the DOM.

How do I test a React form submission?

Type into the inputs, submit, then assert on the success or error state the component renders:

vibe.find('input[name="email"]').type("user@example.com")
vibe.find('input[name="password"]').type("secret123")
vibe.find('button[type="submit"]').click()
 
# The component renders this only after a successful submit.
banner = vibe.find('[data-testid="welcome-banner"]')
assert "Welcome" in banner.text()

Because each find() waits for actionability, the script never clicks a button React has not yet enabled or reads a banner before it mounts.

Tips for stable React tests

  • Assert on rendered elements, not timers — let Vibium wait instead of guessing durations.
  • Use data-testid selectors so component refactors and styling changes do not break tests.
  • Verify content such as a heading or row text, not just that a click happened.
  • Run headless in CI with browser.launch(headless=True) for speed, headed locally to watch the run.

Next steps

Frequently asked questions

How do I test a React app with Vibium?

Launch Vibium, navigate to your React app, then drive it with find(), click(), and type(). Vibium auto-waits for each element to be actionable, so it handles React's asynchronous rendering for you. Assert on the text or state the component renders rather than adding manual sleeps.

Does Vibium work with React, Next.js, and Vite apps?

Yes. Vibium drives a real Chrome browser over WebDriver BiDi, so it tests the final rendered DOM the same way a user sees it. It does not care whether the page was built with Create React App, Next.js, Vite, or Remix — it interacts with the components React paints to the screen.

Why do my React tests flake without Vibium?

React re-renders asynchronously, so a script that reads an element immediately after a click often races the render. Vibium's built-in actionability waiting polls until the element is visible and enabled before interacting, which removes the timing races that cause flaky React tests in CI.

Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.

Related guides