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.
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?
vibe.go(url)— load the SPA's entry point and wait for the initial render.find('a[...]').click()— trigger a client-side route change instead of a server navigation.vibe.find("h1.dashboard-title")— wait for and grab the element the new view renders. Finding it is the wait.heading.text()— read the rendered text to assert the route loaded the right content.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/settingsWhy 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-testidselectors 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
How to Download a File with Vibium
Trigger and save a browser download with Vibium in Python — use capture.download() to grab the file, read its name, and save it with save_as().
2 min read→How-To RecipesHow to Fill and Submit a Form with Vibium
Automate an HTML form with Vibium in Python — type into text fields, check boxes, pick dropdown options, submit, and verify the result with auto-waiting.
2 min read→How-To RecipesHow to Scrape a Table with Vibium
Extract rows and cells from an HTML table with Vibium in Python — find the rows with findAll(), read each cell's text with a scoped find, and build clean structured data.
3 min read→How-To RecipesHow to Take a Full-Page Screenshot with Vibium
Capture an entire scrolling web page as a single PNG with Vibium in Python — the full-page screenshot pattern, runnable code, and a step-by-step breakdown.
3 min read→