VLearnVibium

How to Test a Single-Page App (SPA) with Vibium

Test a React, Vue, or Angular SPA with Vibium in Python — handle client-side routing, wait for async content, and assert on dynamically rendered elements.

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

To test a single-page app with Vibium, navigate in, drive it with find() and click(), and assert on the element each route renders — Vibium auto-waits, so async rendering does not flake. SPAs swap views without a full reload, and Vibium's built-in actionability waiting is exactly what makes that reliable.

What is the SPA test script?

from vibium import browser_sync as browser
 
vibe = browser.launch()
vibe.go("https://app.example.com")
 
# Click a client-side route link — no full page reload happens.
vibe.find('a[href="/dashboard"]').click()
 
# Assert on what the new route renders. find() waits until it exists.
heading = vibe.find("h1.dashboard-title")
assert heading.text() == "Dashboard"
 
vibe.quit()

The script clicks an in-app link and then asserts on the heading the dashboard route renders. Because find() waits for the element, you never race the framework's render.

How does each step work?

  1. vibe.go(url) — load the SPA's entry point and wait for the initial render.
  2. find('a[...]').click() — trigger a client-side route change instead of a server navigation.
  3. vibe.find("h1.dashboard-title") — wait for and grab the element the new view renders. Finding it is the wait.
  4. heading.text() — read the rendered text to assert the route loaded the right content.
  5. vibe.quit() — tear down the browser.

The big difference from a classic multi-page site: there is no full reload to wait on. You wait on the element, and Vibium handles that automatically.

How do I wait for async data to load?

When a view fetches data after rendering, assert on the element that only appears once the data arrives. Vibium polls until it is actionable, so you do not write sleeps:

vibe.find('button[data-testid="load-users"]').click()
 
# This waits until the first user row is actually rendered.
first_user = vibe.find(".user-list .user-row")
print(first_user.text())
 
# Then read the whole list once it is present.
rows = vibe.findAll(".user-list .user-row")
print(f"Loaded {len(rows)} users")

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

How do I assert on a route change?

After an in-app navigation, find the element the new view renders, then confirm the URL with url():

vibe.find('a[href="/settings"]').click()
 
# Finding the panel waits until the settings view has rendered.
heading = vibe.find("#settings-panel h2")
assert heading.text() == "Settings"
 
print(vibe.url())  # https://app.example.com/settings

Why does this beat sleep-based testing?

Manual sleep() calls are the number-one cause of flaky SPA tests: too short and they race the render, too long and the suite crawls. Vibium's auto-wait sidesteps both — it polls the specific element you asked for until it is actionable, then proceeds immediately. Fast machines run fast; slow CI runners still pass.

Tips for stable SPA tests

  • Assert on elements, not timers — let Vibium wait, do not guess durations.
  • Use data-testid selectors so component refactors do not break your tests.
  • Verify the rendered content, for example a heading or row, rather than just the URL.

Next steps

Frequently asked questions

How do I test a single-page app with Vibium?

Navigate to the SPA, then interact with elements using find() and click(). Vibium auto-waits for each element to be actionable, which handles the async rendering that breaks naive scripts. Assert on the content that appears after a route change rather than on a full page reload.

How does Vibium handle client-side routing in SPAs?

SPAs change the view without a full reload. Vibium tracks the URL and auto-waits for elements, so after clicking a link you find() the element that the new route renders. Because finding an element waits until it exists, you do not need to listen for a navigation event.

Why do my SPA tests pass locally but flake in CI?

Flaky SPA tests almost always come from manual sleeps that race async rendering. Vibium's auto-wait fixes this: interact with and assert on the specific element you expect, and Vibium polls until it is actionable, so slow CI machines do not break the run.

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

Related guides