Getting Started
browser.start() (or vibium start in the CLI) must be called before navigation. Skipping it causes silent failures on the first navigate call.browser.stop() in a finally block.headless: true only when you need to hide it.headless defaults to false. During development that's exactly right: you can watch actions, spot wrong selectors, and see dialog popups as they appear. Pass headless: true explicitly for CI runs or batch jobs where a visible window adds no value.Element Selection
find({ role: "button", text: "Submit" }) is more resilient than find(".btn-primary") and also validates that the element is correctly labelled for screen readers.page.find({ text }) returns the outermost matching element.role to narrow the match. Text-only selectors frequently select a wrapper div that can't receive clicks.findAll() when you need every match — find() silently returns the first one.find() always resolves to the first element that matches, even when many do. It does not throw on multiple matches. Use findAll() when you need the complete set — all rows in a table, all error messages, all checkboxes — and then index or iterate over the result..sc-abc123 break silently after a deploy. Use data-testid, ARIA roles, or visible text instead.parent.find(selector) searches within parent, not the whole page. Use this to disambiguate elements that appear in multiple sections — e.g. "Edit" buttons in a list of cards.isVisible() returns false for display:none, visibility:hidden, and zero-opacity elements. Clicking an invisible element either silently no-ops or throws — both hide the real bug.attr() to read non-visible state.aria-expanded, data-state, and custom attributes carry state that isn't visible in rendered text. Read them to assert before and after interactions.isEnabled() catches disabled form controls.isEnabled() before asserting a click had an effect; otherwise you may assert on stale state from before the button became disabled.isChecked() is the correct assertion for checkboxes and radios.isChecked() reads the actual DOM property, which is what the browser and assistive technology use.value() reads the current input value.fill() actually landed in the element rather than being swallowed by a focus guard.Interactions
fill() clears the field before typing.type(), fill() replaces the current value atomically — and works on <textarea> elements as well as <input>. Use it for any text field unless you specifically need to test append or cursor-position behavior.type() when the app listens to individual key events.fill() bypasses these; type() fires keydown, keypress, and keyup for each character.click() includes auto-wait for visibility and enabled state.dblclick() for controls that require a double-click.click() calls — the browser treats them as different events.selectOption() matches by value attribute, not visible text.check() and uncheck() are safer than click() on checkboxes.check() does nothing if the box is already checked. click() would toggle it to unchecked — wrong when prior test state is unknown.hover() to trigger tooltip and dropdown states.dragTo() doesn't trigger custom drag listeners, use mouse.down() → mouse.move() → mouse.up() to build the sequence manually with precise coordinates.scrollIntoView() before interacting with off-screen elements.focus() + keyboard.press() to test keyboard-only workflows.click() as a proxy for keyboard activation. Explicit focus() + keyboard.press("Enter") tests the real keyboard path and catches issues with keydown vs click handlers.Waiting & Synchronization
waitUntil.loaded() after visible text is the most reliable async confirmation.waitUntil(fn) is the escape hatch for custom conditions.page.waitUntil("() => window.__app.ready === true")
go() waits internally, so no explicit wait is needed there. But click() and form submits don't wait for the resulting navigation — you must call waitUntil.loaded() or waitUntil.url(pattern) yourself.waitUntil.url() after OAuth redirects.page.wait(ms) between steps mimics real user pacing.waitUntil(fn) instead of looping with wait().waitUntil(fn) handles polling internally and throws a clean, descriptive timeout error when the condition isn't met. Always pass a full function expression — bare expressions time out regardless of the condition's actual state.Screenshots & Visual Testing
pdf() for print-layout testing — but only in headless mode.pdf() only works when the browser is running headless; calling it with a visible window returns an error.screenshot_001.png tells you nothing. checkout_payment_step_filled.png makes a failing test self-documenting without needing to cross-reference a log file.{ fullPage: true } when relevant content requires scrolling. Viewport-only screenshots miss footer issues, lazy-loaded content, and sticky element overlap problems.vibium highlight to annotate elements before screenshotting.vibium highlight "<selector>" overlays a colored outline for three seconds, making a follow-up screenshot immediately readable. This is CLI-only — the JS, Python, and Java APIs don't expose a highlight method; use dispatchEvent or inject a style via evaluate() to achieve the same effect.vibium diff map detects interactive element changes between steps.vibium diff map compares the current element map against the previous one — showing which labelled interactive elements appeared, disappeared, or changed. It is a structural diff of the page's interactive surface, not a pixel diff of screenshots.Dialog Handling
dialog.dismiss() to test cancellation flows.dialog.message() before accepting or dismissing.onDialog when you need to assert or control the outcome.dialog.dismiss() when a dialog appears. The page won't hang. Register onDialog (or use capture.dialog) when you need to assert the message text, accept instead of dismiss, or supply input to a prompt.prompt dialogs accept an input value as an argument to accept().dialog.accept("my-input-value"). This enables testing input-dependent prompt flows — confirmation codes, rename dialogs, and similar.fn().then(() => dialogPromise) — fn is fully awaited before the dialog promise is checked. If fn awaits a click that blocks until the dialog is dismissed, fn never resolves and the dialog promise is never reached: deadlock. Fire the click without await and add .catch(() => {}) to suppress the unhandled rejection that fires when the blocked click eventually aborts.
// WRONG — deadlock const dialog = await page.capture.dialog(async () => { await page.find({ role: 'button', text: 'Delete' }).click(); }); // CORRECT — fire-and-forget with catch const dialog = await page.capture.dialog(async () => { page.find({ role: 'button', text: 'Delete' }).click().catch(() => {}); });
Network & Routes
**/api/** pass-through last, so it doesn't shadow the routes you actually care about.route.continue() for unmatched routes in partial mocks.page.unroute(pattern) explicitly in teardown, not just at process exit.Storage & Cookies
context.cookies() to assert session cookies after login.context.setCookies() seeds auth state without going through the login UI.context.clearCookies() is the cleanest way to test logged-out state.context.storage() snapshots and context.setStorage() restores full browser state.setStorage() doesn't navigate — call go() after restoring.page.clock to test JavaScript-driven session expiry — not HTTP cookie expiry.page.clock overrides Date.now(), setTimeout, and related APIs in the page's JavaScript context. It does not affect the browser's HTTP-level cookie jar. Use the clock to test apps that check expiry in JS — e.g. reading a JWT exp claim or running a countdown timer. For HTTP-level cookie expiry, set short-lived cookies and wait, or clear them manually with clearCookies().Multi-Page & Frames
browser.pages() to enumerate open tabs.target="_blank" links open new pages. pages() lets you find and switch to them by title or URL instead of guessing page indices.page.close().page.frame() returns an iframe as a new Page context.page.frame(nameOrUrl) and interact through it. Forgetting this produces "element not found" errors for elements that are clearly visible.page.frames() to find the right frame by URL or name.name attribute rather than hard-coding a frame index that breaks when the page structure changes.page.frame() returns a new Page object scoped to that frame. Your original page variable still points to the parent. Store both explicitly if you need to switch between them.Keyboard & Mouse
page.keyboard.press() sends keyboard shortcuts to the focused element.page.keyboard.press("Control+a") selects all text in the focused input. Use modifier+key combinations for shortcuts; don't simulate them by clicking selection handles.element.press() sends a single key while the element is focused."Enter", "Escape", "Tab", "ArrowDown". Raw character strings work for printable characters but are ambiguous for function and navigation keys.mouse.move() before mouse.down() for drag simulations.mousemove. Moving to the start position before pressing gives the app a starting coordinate it can use to compute drag delta.mouse.click() is left-button only.vibium mouse click x y --button 2. MCP: browser_mouse_click with button: 2. In the JS API, page.mouse.click(x, y) has no button parameter and always fires button 0. For right-click in the JS API, dispatch a contextmenu event via page.evaluate().document.activeElement.page.evaluate("document.activeElement.id") tells you which element has focus after each Tab press. This is the only reliable way to automate WCAG 2.4.3 focus order testing.uncheck() then check() when state is unknown.uncheck → check guarantees a known starting state without reading current state first.Recording & Debugging
recording.start() / recording.stop() captures a trace archive of the test run.page.context.recording.start(). CLI: vibium record start.recording.startGroup(name) to annotate phases of a long test.page.evaluate("performance.getEntriesByType('navigation')[0].loadEventEnd").page.content() reveals the raw DOM when visual inspection isn't enough.page.title() is the fastest sanity check after navigation.title() returns something unexpected, you're on the wrong page. This single assertion catches redirect failures, auth walls, and 404 pages before deeper assertions produce confusing errors.page.a11yTree() exposes the full accessibility tree.Performance & Scale
page.setViewport({ width: 375, height: 812 }) emulates an iPhone viewport. Mobile-specific breakpoints, touch targets, and nav patterns are invisible at desktop width.page.emulateMedia() toggles dark mode and print media without OS changes.page.emulateMedia({ colorScheme: "dark" }) applies dark mode instantly. Test dark/light mode visual regression in the same CI run without system-level configuration changes.page.setGeolocation() tests location-aware features without a real device.Testing Patterns
page.clock to test time-dependent UI without waiting.Open Issues
5 confirmed open issues on VibiumDev/vibium map directly to tips in this article. Gold badges on tip cards link to the relevant issue.
The Thread Running Through All 99
Every tip here traces back to the same idea: test what the user experiences, not how the code is written. Selectors that survive refactors, waits tied to visible state, assertions on user-facing text — these hold up because they model the product from the outside, the same way a user does.
The second thread is determinism. Sleeps, index-based selectors, and shared state are the three root causes behind almost every flaky test. Replace each one with a condition-based wait, a semantic selector, and isolated state — and a suite that flakes 30% of the time becomes one that fails when it should and passes when it should.
Vibium gives you the tools. These 99 tips are the judgment layer on top — knowing which tool to reach for, and why.