The CLI is a terminal. The MCP server is a tool list. The TypeScript/JavaScript API is a library — async, typed, and structured around objects that hold state. You import it, you instantiate a browser, you get a page, and every method is a promise. It's the surface you reach for when automation lives inside a Node.js codebase and needs to compose cleanly with everything else already there.
reload · url · title
content
page.findAll
type · check · uncheck
select · focus · hover
scroll · text · innerText
value · getAttribute
bounds · isVisible
isEnabled · isChecked
isReadOnly
down · up
down · up
waitUntil.url
waitUntil.loaded
setContent · addScript
addStyle · expose
setViewport · evaluate
capture.navigation
capture.response
capture.request
consoleMessages
route · unroute
setHeaders
newContext
pages
setSystemTime
setTimezone
fastForward · pauseAt
resume · runFor
Starting up
Install with npm install vibium, then import and start. The entry point is
Vibium.start() — it returns a Browser instance. From there,
every operation flows through the object model: browser → page → element.
const browser = await Vibium.start();
const page = await browser.newPage();
await page.go('https://github.com/login');
const input = await page.find({ role: 'textbox', label: 'Username or email address' });
await input.fill('your-username');
await browser.close();
Three lines to go from import to first interaction. The async/await model is consistent
throughout — every method on Page, Element, and
Browser returns a promise. There are no callbacks, no event emitters,
no synchronous steps.
Pass { headless: true } to Vibium.start() for CI runs.
The default is headed — the browser window is visible, which is what you want during
development when you're watching what the script does.
Navigating
Seven methods, all on page. page.go(url) navigates and waits
for load by default. Pass an options object with waitUntil to control the
load condition — "networkidle" for SPAs, "commit" when you
want to proceed immediately after the navigation commits without waiting for content.
await page.go('https://app.example.com', { waitUntil: 'networkidle' });
const url = await page.url();
const title = await page.title();
// use as assertions
assert(title === 'Dashboard');
page.url() and page.title() are the most common assertion
primitives in navigation-heavy test suites — lightweight, readable, and independent of
any element on the page. page.content() returns the full page HTML as a
string, useful when you need to inspect raw markup without querying individual elements.
page.back(), page.forward(), and page.reload()
mirror browser button behaviour. All three wait for the page to settle before resolving,
so you can call page.find() immediately after without sleeping.
Finding elements
Two methods. The entire element interaction model flows through these two. page.find()
returns a single Element handle — an object with 19 methods on it.
page.findAll() returns an array of them.
const btn = await page.find({ role: 'button', text: 'Sign in' });
const input = await page.find({ role: 'textbox', label: 'Email' });
// By placeholder, alt, or test ID
const search = await page.find({ placeholder: 'Search…' });
const avatar = await page.find({ alt: 'User avatar' });
const widget = await page.find({ testid: 'price-widget' });
// All matching elements
const rows = await page.findAll('table tbody tr');
for (const row of rows) {
const text = await row.text();
console.log(text);
}
The semantic locators — role, label, placeholder, alt, testid — are the right default. They survive HTML refactors as long as the UX intent stays the same. Reach for CSS selectors when none of those apply, and XPath when CSS can't express the query.
page.find() waits for the element to appear before returning. The default
timeout is 30 seconds — pass a timeout property in the selector object to
override it. If the element doesn't appear within the timeout, the promise rejects with
a descriptive error.
Element interactions
Nineteen methods on the Element object returned by page.find().
The pattern is consistent: find once, interact many times on the same handle.
email: await page.find({ role: 'textbox', label: 'Email' }),
password: await page.find({ role: 'textbox', label: 'Password' }),
submit: await page.find({ role: 'button', text: 'Sign in' }),
};
await form.email.fill('user@example.com');
await form.password.fill('secret');
await form.submit.click();
el.fill() clears the field and types in one operation. el.type()
appends character-by-character — use it when autocomplete needs to fire on each keystroke.
el.select() picks a <select> option by its visible label
or value attribute.
The four read methods cover the full spread of what you need from an element's content.
el.text() is raw textContent — includes hidden text, line breaks
from block-level children. el.innerText() is the rendered version — what a
user would actually see and copy. el.value() reads the current value of any
form input. el.getAttribute(name) retrieves any HTML attribute by name.
The four boolean methods are guard tools. Check el.isEnabled() before clicking
a submit button if you're verifying that the form's validation state is correct.
el.isVisible() tests whether the element is in the viewport and not hidden by
CSS. el.isChecked() reads checkbox and radio state.
el.isReadOnly() checks the readonly attribute on form fields.
el.bounds() returns the element's position and dimensions as
{ x, y, width, height } — useful for layout assertions or for computing
coordinates when you need to interact with a specific region inside an element.
Keyboard and mouse
Eight methods across two sub-objects. The keyboard and mouse APIs are the low-level layer beneath the element methods — reach for them when you need precise control over input timing, key sequences, or coordinates.
await page.keyboard.press('Control+a');
await page.keyboard.press('Shift+Tab');
// Hold shift while pressing arrows
await page.keyboard.down('Shift');
await page.keyboard.press('ArrowRight');
await page.keyboard.press('ArrowRight');
await page.keyboard.up('Shift');
// Raw mouse — for canvas or custom drag targets
await page.mouse.move(120, 240);
await page.mouse.down();
await page.mouse.move(380, 240);
await page.mouse.up();
page.keyboard.down() and page.keyboard.up() are the modifier-key
pair — hold a key across multiple other key events. The example above selects two characters
to the right using shift+arrow, which no single press() call can express.
The mouse API operates on raw page coordinates. The sequence move → down → move → up
is how you implement a custom drag gesture on a target that el.scroll() can't
reach — a canvas slider, a map pane, a custom range control built without semantic HTML.
Waiting
Four methods. page.wait(ms) is the unconditional sleep — fixed milliseconds,
always blocking. Use it sparingly. The other three are conditional: they resolve the moment
their condition is met, which makes tests faster and less brittle than a fixed sleep.
await page.waitUntil('document.querySelector(".spinner") === null');
// Wait for URL after form submission redirect
await page.waitUntil.url('https://app.example.com/dashboard');
await page.waitUntil.url('**/dashboard**');
await page.waitUntil.url(/dashboard/);
// Wait for full page load after navigation
await page.waitUntil.loaded();
page.waitUntil(expression) evaluates a JavaScript string in the page context
on a polling interval and resolves when the expression returns truthy. It's the general-purpose
wait — use it for application-specific conditions that no CSS selector can express: a flag
on window, a data attribute set by app logic, a third-party widget's internal
state.
page.waitUntil.url() accepts a string, a glob, or a regex. Globs are the
pragmatic choice after redirects where the exact final URL varies by environment — a pattern
like '**/dashboard**' matches regardless of the host.
Page utilities and network capture
Seventeen methods split across two categories. The Page methods modify or observe the page as a whole. The Capture methods intercept events — dialogs, navigations, network requests — that would otherwise be hard to test because they happen asynchronously in response to an action.
const count = await page.evaluate('document.querySelectorAll("li").length');
// page.addStyle — inject CSS to suppress flaky animations in tests
await page.addStyle('*, *::before, *::after { animation-duration: 0s !important; }');
// page.setContent — render a component without a dev server
await page.setContent('<div class="card"><h2>Hello</h2></div>');
// page.setViewport — simulate a specific device before navigation
await page.setViewport({ width: 390, height: 844 }); // iPhone 15
The page.capture.* methods handle the hardest category of browser interaction
to test: things that happen in response to another action. The key rule is that the action
triggering the event must be fire-and-forget inside the capture callback — never awaited.
const dialog = await page.capture.dialog(async () => {
page.find({ role: 'button', text: 'Delete account' }).then(b => b.click());
});
await dialog.accept();
// Intercept an API response triggered by a button click
const res = await page.capture.response('/api/users', async () => {
const btn = await page.find({ role: 'button', text: 'Load users' });
await btn.click(); // this is fine — it's inside capture.response's fn
});
const data = await res.json();
page.route() intercepts all requests matching a URL pattern and routes them
through a handler. Use it to mock API responses in tests — return a fixed JSON payload
without hitting the real server. page.unroute() removes the handler when
the test is done.
page.setHeaders() adds custom HTTP headers to every subsequent request from
the page — the clean way to inject an auth token for tests that can't use cookie-based
sessions.
Browser context and the virtual clock
The three browser.* methods manage multi-page and multi-context sessions.
browser.newPage() opens a fresh tab in the current context — it shares
cookies and storage with existing pages. browser.newContext() creates an
isolated context with its own cookies, storage, and permissions — the right choice when
you need two sessions running in parallel without interference, such as testing an
admin view alongside a user view.
const adminCtx = await browser.newContext();
const userCtx = await browser.newContext();
const adminPage = await adminCtx.newPage();
const userPage = await userCtx.newPage();
await adminPage.go('https://app.example.com/admin');
await userPage.go('https://app.example.com/dashboard');
The Clock API replaces the browser's real-time functions with a controllable fake.
Call clock.install() once at the start of the test — after that,
Date.now(), setTimeout, setInterval, and all
related browser APIs respond to the clock you control.
clock.install({ now: new Date('2026-01-01T09:00:00Z') });
await page.go('https://app.example.com/session');
// Jump 30 minutes into the future
clock.fastForward(30 * 60 * 1000);
// Verify the session expired banner appeared
const banner = await page.find({ role: 'alert', text: 'Session expired' });
assert(await banner.isVisible());
clock.setTimezone() accepts any IANA timezone name and overrides what the
page sees as its local timezone — making locale-sensitive UI logic fully testable without
changing the system's actual timezone or running a VM. clock.runFor(ms)
advances the clock by a duration and fires all timers that fall within that window,
then pauses — the right tool for testing that a setInterval callback fires
the expected number of times.
68 methods. 10 categories. One async object model from browser to page to element.
The pattern worth internalising is the separation between find and act: resolve the
element handle once with page.find(), then call methods on the handle as
many times as you need. It's more readable than chaining finders, and it makes it
obvious at a glance what the test is targeting.
The next issue covers the Python API — 72 methods across the same surface, with
snake_case naming and synchronous-style syntax made async-friendly through
Python's native async/await.