10 Common Vibium Mistakes and How to Fix Them
10 common Vibium mistakes and how to fix them — manual sleeps, brittle CSS, missing quit(), sync/async mix-ups, iframe misses, and headless-only bugs.
The most common Vibium mistakes are timing and lifecycle habits carried over from older tools: adding manual sleeps that Vibium's auto-waiting already handles, leaning on brittle structural CSS selectors, forgetting to call quit() or close(), and mixing the sync and async APIs. Vibium is AI-native browser automation built on WebDriver BiDi by Jason Huggins, the co-creator of Selenium and Appium, and it ships as a single Go binary that auto-downloads Chrome. Because the engine auto-waits for actionability, most reliability problems come from writing against it as if it were Selenium 3. This guide walks through ten concrete mistakes — with the wrong code, the fixed code, and why — covering sleeps, selectors, cleanup, sync/async confusion, iframes, ignored return values, headless-only failures, over-broad AI checks, hard-coded waits for network, and skipping error handling. Fix these and your scripts get faster, shorter, and far less flaky.
What does the fix workflow look like?
Most Vibium mistakes fall into a short pipeline: you find the element, act on it, let the engine wait, verify the result, then clean up. Getting each stage right in that order removes the majority of flakiness.
The sections below map onto this flow. Finding and acting are where selector and sync mistakes live; auto-wait is where sleeps go wrong; verify is where ignored return values and AI checks matter; and quit is the cleanup step almost everyone forgets at least once.
Here is a quick reference before the deep dive.
| # | Mistake | Quick fix |
|---|---|---|
| 1 | Manual sleep() everywhere | Delete them; rely on auto-waiting |
| 2 | Brittle structural CSS | Use find(role=..., text=...) |
| 3 | Never calling quit() / close() | Close in a finally block |
| 4 | Mixing sync and async imports | Pick one API and stay in it |
| 5 | Searching the wrong document | Switch into the iframe first |
| 6 | Ignoring find() return values | Read text(), value(), state |
| 7 | Works headed, fails headless | Test headless + set a viewport |
| 8 | Vague check() claims | Make claims specific and scoped |
| 9 | Racing the network | waitForResponse, not sleep |
| 10 | No error handling / screenshots | Wrap in try/except, capture PNG |
Mistake 1: Adding manual sleeps
The single most common Vibium mistake is sprinkling fixed sleeps between actions. Vibium auto-waits for every element to be present, visible, and actionable before it clicks, types, or reads it, so a hard-coded pause is redundant at best and a race condition at worst.
A two-second sleep wastes two seconds when the page is fast, and still fails when a slow network takes three. Auto-waiting adapts to the actual page.
# Wrong: guessing at timing
from vibium import browser_sync as browser
import time
vibe = browser.launch()
vibe.go("https://example.com/login")
time.sleep(2) # brittle: too long or too short
vibe.find("#email").type("user@test.com")# Right: let Vibium wait for actionability
from vibium import browser_sync as browser
vibe = browser.launch()
vibe.go("https://example.com/login")
vibe.find("#email").type("user@test.com") # waits until the field is ready
vibe.quit()Why does auto-waiting beat a sleep? Because a sleep encodes a guess about how long the page will take, and that guess is wrong in both directions. On a warm cache the field is ready in 80 milliseconds and your two-second sleep burns the other 1,920. On a cold start or a throttled CI runner the field is not ready for three seconds and your sleep fires the action into an element that does not exist yet. Vibium's engine sidesteps the guess entirely: it polls the element until it is present, visible, stable, and enabled, then acts — so the same script is fast on fast pages and patient on slow ones.
The only legitimate use for a fixed wait is a deliberate pause you cannot express as a condition — a demo that needs a visible beat, say. Even then, prefer waiting for a specific element or response. See flaky clicks for the actionability model in depth.
Mistake 2: Relying on brittle CSS selectors
Long structural CSS paths break the moment the page layout changes. A selector like #root > div:nth-child(2) > form > button encodes the DOM tree, so any wrapper div a designer adds shatters it.
The durable fix is to describe the element the way a user perceives it — by accessible role, visible text, or label — using Vibium's semantic find().
# Wrong: tied to structure, breaks on any redesign
vibe.find("#root > div:nth-child(2) > form > button").click()
# Right: matches by role and visible text, like a user reads it
vibe.find(role="button", text="Sign up").click()// JavaScript, sync API — same idea
const { browser } = require('vibium/sync')
const bro = browser.launch()
const page = bro.page()
page.go('https://example.com/signup')
page.find({ role: 'button', text: 'Sign up' }).click()
bro.close()find() accepts role, text, label, placeholder, alt, title, and testid. Reach for a short CSS selector only for stable hooks like #email or a data-testid. Everything else should be semantic. The full reference lives at find element.
Mistake 3: Never closing the browser
Forgetting to call vibe.quit() in Python or bro.close() in JavaScript leaves the browser process and its Chrome instance running after your script exits. Run a suite a few hundred times in CI and those orphaned processes exhaust memory.
Put cleanup in a finally block so it runs even when a step throws an exception mid-script.
from vibium import browser_sync as browser
vibe = browser.launch()
try:
vibe.go("https://example.com")
vibe.find(role="link", text="Docs").click()
finally:
vibe.quit() # always runs, even on errorconst { browser } = require('vibium/sync')
const bro = browser.launch()
try {
const page = bro.page()
page.go('https://example.com')
page.find({ role: 'link', text: 'Docs' }).click()
} finally {
bro.close() // always runs, even on error
}This one habit prevents the slow memory creep that makes long-running CI agents fall over after enough iterations.
Mistake 4: Mixing the sync and async APIs
In Python, from vibium import browser_sync as browser gives you a blocking API with no await, while from vibium.async_api import browser returns awaitables. Mixing them — awaiting a sync call, or forgetting await on an async one — produces confusing "coroutine was never awaited" errors or silent no-ops.
Pick one style per script and stay in it. Sync is the simpler default for scripts and most tests.
# Sync — no await anywhere
from vibium import browser_sync as browser
vibe = browser.launch()
vibe.go("https://example.com")
title = vibe.title()
vibe.quit()# Async — await every call, run inside an event loop
import asyncio
from vibium.async_api import browser
async def main():
bro = await browser.launch()
vibe = await bro.new_page()
await vibe.go("https://example.com")
await bro.close()
asyncio.run(main())The tell-tale symptoms are worth memorizing. Forgetting await on an async call gives you a coroutine object instead of a result, so vibe.title() returns something like <coroutine object ...> and Python prints a "coroutine was never awaited" warning. Adding await to a sync call raises a TypeError because a plain string or Element is not awaitable. Both errors point at the same root cause: the import and the call style disagree.
The JavaScript equivalent is require('vibium/sync') for the blocking API versus require('vibium') for the promise-based one. The same rule applies — never await a value from the sync client, and always await one from the async client. Choose sync unless you genuinely need concurrency across several pages at once. If you are new to the clients, start with what is Vibium and install Vibium.
Mistake 5: Searching the wrong document (iframes)
When Vibium reports an element as not found even though you can see it, the element is often inside an iframe. An iframe is a separate document, so a top-level selector cannot reach into it — Vibium searches only the main document unless you switch into the frame's context first.
# Wrong: the payment button lives inside an <iframe>,
# so a main-document search never finds it.
vibe.go("https://example.com/checkout")
vibe.find(role="button", text="Pay now").click() # not found
# Right: enter the frame context, then find within it.
# Consult vibium.com for the exact frame API in your version.
vibe.go("https://example.com/checkout")
# switch into the payment iframe, then:
vibe.find(role="button", text="Pay now").click()In WebDriver BiDi — the W3C standard Vibium is built on — frames are addressable browsing contexts with the same find API as a page. That is a real improvement over Selenium's switchTo().frame() / switchTo().defaultContent() juggling: once you hold a reference to the frame, you call find() on it directly and never have to remember to switch back out. The mistake is not iframes themselves; it is forgetting that the element lives in a child document at all. A quick way to catch it is to open the page, right-click the element, and check whether an <iframe> wraps it in the DOM tree.
If the exact frame-access method name for your Vibium release is unclear, check the official docs at vibium.com rather than guessing. See also element not found.
Mistake 6: Ignoring what find() returns
A frequent oversight is treating find() as fire-and-forget and never reading the element's state. find() returns an Element you can query — text(), value(), attr(), isVisible(), isEnabled(), isChecked() — and skipping those reads means your script acts blindly.
# Wrong: assumes the button is ready without checking
vibe.find(role="button", text="Submit").click()
# Better: confirm state before you act on it
btn = vibe.find(role="button", text="Submit")
if btn.is_enabled():
btn.click()
# Read values instead of guessing
total = vibe.find("#cart-total").text()
print("Cart total:", total)Reading state turns silent failures into clear branches: you can log the actual value, skip a disabled control, or assert on real text instead of hoping the click did something.
Mistake 7: Assuming headless behaves like headed
Scripts that pass on your machine but fail in CI usually hit a headed-versus-headless gap. Locally you run headed and slow, which hides timing bugs; CI runs headless and fast, with a different default viewport, so elements that were visible on your screen may be off-screen or not yet rendered.
Reproduce CI locally by running headless with an explicit viewport before you push.
from vibium import browser_sync as browser
# Match CI: headless, fixed viewport
vibe = browser.launch(headless=True)
vibe.go("https://example.com")
# ... your steps ...
vibe.quit()Two rules prevent most headless-only failures: never depend on a manual sleep (mistake 1), and set a viewport so layout is deterministic. If a page element sits below the fold, Vibium scrolls it into view before acting — but only if your selector actually matches it. For headless crashes specifically on Linux CI, see headless crash on Linux.
| Symptom | Headed (local) | Headless (CI) | Fix |
|---|---|---|---|
| Element off-screen | Visible, works | Not in viewport | Set explicit viewport |
| Fast page transition | Human-speed, hidden | Race exposed | Rely on auto-wait |
| Missing OS deps | Present on your Mac | Absent on runner | Install Chrome libs |
| Different fonts/render | Looks fine | Layout shifts | Assert on text, not pixels |
Mistake 8: Writing vague AI check() claims
Vibium's AI-native check() verifies plain-English claims against the page, but a vague claim gives a vague or unstable result. "It works" is unverifiable; "the cart badge shows 0 items" is a concrete, repeatable assertion.
Make claims specific and, where possible, scope them to a region with the near option so the model looks at the right part of the page.
// Wrong: too vague to verify reliably
await page.check('the page is fine')
// Right: specific, and scoped to a region
await page.check('the cart icon shows 0 items')
await page.check('shows 3 search results', { near: '.search-results' })
const { passed, reason } = await page.check('no validation error is visible')
if (!passed) throw new Error(reason)A second, subtler version of this mistake is reaching for check() when a plain read would be exact and free. If you know the selector and the expected value, page.find('#cart-count').text() returning "0" is deterministic and instant; asking an AI model "is the cart empty?" adds latency and a small chance of ambiguity. check() earns its place when the assertion is genuinely visual or hard to pin to one node — "the error banner is red," "the layout is not broken," "the results look sorted." Use the deterministic read when you can, and the AI claim when you must.
check() complements the deterministic API — it does not replace el.text(). Use a precise text() read when you know the exact selector, and a scoped check() when you want to assert on intent or appearance. Treat both as assertions with clear pass/fail conditions so a failure is unambiguous.
Mistake 9: Racing the network with sleeps
Waiting for an API call to finish by guessing a sleep is fragile. If a search fires an XHR and you sleep two seconds before reading results, you fail whenever the backend is slow and waste time whenever it is fast — the same anti-pattern as mistake 1, but for network instead of the DOM.
Wait for the actual response or for the element the response produces.
const { browser } = require('vibium/sync')
const bro = browser.launch()
const page = bro.page()
page.go('https://example.com/search')
page.find('#q').type('vibium')
page.find('button[type=submit]').click()
// Wait for the real result element, not a fixed delay
page.waitFor('.search-results .result')
console.log('Results loaded')
bro.close()If you need the response payload itself, Vibium exposes waitForResponse(pattern) to block until a matching request completes, then read its status or JSON. Either way, wait on a condition, never on a stopwatch. The pattern of asserting on both the API response and the rendered DOM is covered in combining API and web testing.
Mistake 10: Skipping error handling and screenshots
When a script fails without any diagnostics, you are left guessing whether the selector was wrong, the element was absent, or the page never loaded. The fix is cheap: wrap risky steps in try/except and capture a full-page screenshot at the point of failure.
from vibium import browser_sync as browser
vibe = browser.launch()
try:
vibe.go("https://example.com")
vibe.find("#promo").click()
except Exception:
with open("/tmp/failure.png", "wb") as f:
f.write(vibe.screenshot(full_page=True))
raise # keep the stack trace
finally:
vibe.quit()Two details make this pattern pull its weight. First, use full_page=True so the image captures content below the fold — the failing element is often just off-screen, and a viewport-only shot would hide it. Second, raise after capturing so you keep the original stack trace; swallowing the exception to "keep the suite green" only hides the failure until it bites in production. In CI, write the PNG to a path your pipeline archives as an artifact so you can open it after the run without re-executing the script.
The screenshot instantly distinguishes "wrong selector" from "element genuinely absent," which tells you whether to fix the locator or the timing. For debugging blank or empty images, see blank screenshots and the screenshot reference.
How do I avoid these mistakes going forward?
The through-line across all ten is: trust the engine, describe elements semantically, and clean up. Vibium already handles the waiting, actionability, and scrolling that older tools made you script by hand, so most fixes are about deleting code, not adding it.
- Delete every fixed sleep — auto-waiting replaces them (mistakes 1, 9).
- Find by role, text, and label — not by DOM position (mistake 2).
- Close in
finally— no orphaned browser processes (mistake 3). - Commit to one API — sync or async, never both in a script (mistake 4).
- Switch into iframes — a top-level search cannot see them (mistake 5).
- Read element state — branch on real values, not assumptions (mistakes 6, 8).
- Test headless with a viewport — reproduce CI before you push (mistake 7).
- Capture a screenshot on failure — turn silent errors into evidence (mistake 10).
Adopt these as defaults and your Vibium scripts stay short, fast, and stable as the pages under test change.
Next steps
Frequently asked questions
What is the most common Vibium mistake?
Adding manual sleeps. Vibium already auto-waits for elements to become actionable before it clicks, types, or reads, so a fixed sleep either wastes time or fires too early on a slow load. Delete the sleeps and let find() and its actions poll for you instead.
Why do my Vibium scripts pass locally but fail in CI?
Local runs are headed and slow enough to hide timing bugs; CI is headless, faster, and has a different default viewport. Elements that were visible on your screen may be off-screen or not yet rendered in CI. Run headless locally, set an explicit viewport, and rely on auto-waiting rather than sleeps.
Do I need to call quit() or close() in Vibium?
Yes. If you never call vibe.quit() in Python or bro.close() in JavaScript, the browser process and its Chrome instance keep running after your script ends. Over many runs these orphaned processes pile up and exhaust memory. Always close in a finally block so cleanup runs even on error.
Why does await not work in my Vibium Python script?
Because the default Python import is synchronous. from vibium import browser_sync as browser gives you a blocking API with no await. Only from vibium.async_api import browser returns awaitables. Mixing the two — awaiting a sync call or forgetting await on an async one — is a frequent Vibium mistake.
Why can't Vibium find an element that is clearly on the page?
The element is often inside an iframe, which is a separate document a top-level selector cannot reach, or it only appears after an earlier action you have not triggered yet. Switch into the frame first, or perform the revealing step before you search. A brittle CSS path that broke on a redesign is the third common cause.
Is it a mistake to use CSS selectors in Vibium?
Not always, but long structural CSS paths like div:nth-child(2) > form > button break on any layout change. Prefer semantic finds — find(role='button', text='Submit') or find(label='Email') — which match elements the way a user perceives them and survive redesigns that shatter positional selectors.
Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.
Related guides
The Complete Vibium Debugging Guide
The complete Vibium debugging guide — verbose logs, actionability checks, screenshots, tracing, and zombie-process fixes to find why a script fails fast.
14 min read→TroubleshootingFixing Flaky Vibium Tests: The Complete Guide
Fixing flaky Vibium tests: diagnose the root cause, replace sleeps with auto-waiting, stabilize selectors, mock the network, and gate CI on green.
15 min read→TroubleshootingFix: Vibium Screenshot Comes Out Blank
Why your Vibium screenshot comes out blank or white, and how to fix it — wait for content, full_page=True, set a viewport, and verify the PNG bytes.
4 min read→TroubleshootingFix: Vibium Chrome Download Fails
Vibium Chrome download fails on first launch? Fix it with vibium install, by clearing network/proxy and disk-space blocks, plus Linux deps.
4 min read→