VLearnVibium

How to Use Vibium with Jest

Use Vibium with Jest to run AI-native browser tests: a shared browser fixture, per-test isolation, screenshot-on-failure, and clean CI runs.

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

To use Vibium with Jest, install both with npm, launch a Vibium browser once in a beforeAll hook, give each test a fresh page or isolated context, drive the UI with find/click/fill, and assert the result with Jest's expect. Vibium is a normal npm package built on WebDriver BiDi, so it drops into a stock Jest setup with no custom preset — you only raise Jest's default timeout because real browser steps outlast unit tests. The async client (require('vibium')) pairs cleanly with Jest's promise support: await each command and let Vibium's auto-waiting handle timing so you never sprinkle sleep() calls. Close the browser in afterAll, and capture a screenshot in afterEach when a test fails so headless CI runs stay debuggable. Because Vibium auto-downloads Chrome for Testing, there is no driver to version-match — the classic source of flaky end-to-end suites. This guide gives you a runnable, JS-first setup plus the fixture patterns that keep a Jest browser suite fast and stable.

How does a Vibium test fit into Jest?

A Vibium Jest test is a normal test() block where the browser is the thing under test. Jest owns the lifecycle (beforeAll, test, afterAll, assertions); Vibium owns the browser (launch, navigate, find, act). The two never fight — Jest awaits Vibium's promises, and Vibium's actionability engine waits for the page. The pipeline below is what every Vibium Jest run looks like end to end.

Jest starts the run and fires a hook; the hook launches a Vibium browser and opens a page; your test acts on that page and reads state back; expect asserts the state; a teardown hook closes the browser. Keep that mental model and the rest is wiring. If Vibium itself is new to you, start with what is Vibium and install Vibium.

How do I install Vibium and Jest?

Install both packages into your project with a single npm command, then add a Jest config. Vibium ships as one Go binary that downloads Chrome for Testing on first use, so there is no separate browser or driver step.

npm init -y
npm install --save-dev jest
npm install vibium

Add a minimal jest.config.js. The one setting that matters for browser tests is testTimeout — real navigation and rendering take longer than a unit test, so Jest's 5-second default will trip you up.

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  testTimeout: 30000, // browser steps need more than the 5s default
};

Wire up an npm script so the suite runs with one command:

{
  "scripts": {
    "test": "jest"
  }
}

That is the entire toolchain. There is no Vibium-specific Jest preset, transformer, or environment to install — Vibium is a library your test imports, and Jest treats it like any other dependency.

What does a first Vibium Jest test look like?

The simplest working test launches a browser in beforeAll, navigates in the test, asserts on page state, and closes in afterAll. This uses the async client so Jest can await each step.

// login.test.js
const { browser } = require('vibium');
 
let bro;
let page;
 
beforeAll(async () => {
  bro = await browser.launch({ headless: true });
});
 
afterAll(async () => {
  await bro.close();
});
 
beforeEach(async () => {
  page = await bro.newPage();
});
 
afterEach(async () => {
  await page.close();
});
 
test('the homepage shows the site title', async () => {
  await page.go('https://example.com');
 
  const heading = page.find('h1');
  expect(await heading.text()).toContain('Example Domain');
});
 
test('the more-information link is present', async () => {
  await page.go('https://example.com');
 
  const link = page.find('a');
  expect(await link.text()).toBeTruthy();
});

Two things make this reliable. First, find() returns immediately but its actions auto-wait for the element to be actionable, so you never guess at timing — read how actionability works for the details. Second, a fresh page per test means one test's navigation never bleeds into the next.

Note that page.find('h1') is synchronous — it returns an Element handle — while reading its value (heading.text()) is awaited. That split lets you write terse locators and only await when you actually touch the browser.

Should I use Vibium's sync or async API with Jest?

Use the async API (require('vibium')) for Jest, because Jest already models tests as promises and reports async failures cleanly. The sync API (require('vibium/sync')) exists for straight-line scripts and quick spikes, but inside a test runner the async style reads better and integrates with Jest's timeout handling.

AspectAsync — require('vibium')Sync — require('vibium/sync')
Test body styleawait page.go(url)page.go(url)
Fit with JestNative — Jest awaits the returned promiseWorks, but blocks the event loop per call
Error/stack clarityAsync stack traces, clean failure reportingFine for simple flows
Parallel pages in one testStraightforward with Promise.allSerial only
Best forTest suites, CI, anything non-trivialOne-off scripts, prototypes, docs snippets

The sync equivalent of the launch-and-navigate snippet is genuinely shorter, which is why it shines outside Jest:

// quick script, not a Jest test
const { browser } = require('vibium/sync');
 
const bro = browser.launch();
const page = bro.page();
page.go('https://example.com');
console.log(page.find('h1').text());
bro.close();

Pick one style per project. Mixing awaited and blocking calls in the same suite makes failures harder to read and confuses teammates about which conventions apply.

How should I structure the browser fixture?

Launch the browser once for the whole file and hand each test an isolated context, not just a page. A context is Vibium's isolation boundary — its own cookies, its own storage — so a login in one test cannot leak into another. Launching Chrome is the expensive part; creating a context is cheap, so you get isolation without paying a per-test startup cost.

// account.test.js
const { browser } = require('vibium');
 
let bro;
let ctx;
let page;
 
beforeAll(async () => {
  bro = await browser.launch({ headless: process.env.HEADLESS === 'true' });
});
 
afterAll(async () => {
  await bro.close();
});
 
beforeEach(async () => {
  ctx = await bro.newContext();  // fresh cookies + storage per test
  page = await ctx.newPage();
});
 
afterEach(async () => {
  await ctx.close();             // wipe this test's state
});
 
test('seeded session lands on the account page', async () => {
  await ctx.setCookies([
    { name: 'session', value: 'test-token', url: 'https://app.example.com' },
  ]);
 
  await page.go('https://app.example.com/account');
  expect(await page.find('h1').text()).toContain('Account');
});

Seeding auth through ctx.setCookies() also skips slow, flake-prone UI logins — you assert the logged-in state directly. For the full login walkthrough, see automate login with Vibium, and for suite-wide organization see structure a Vibium test suite.

If several files share this fixture, lift it into a Jest setupFilesAfterEach-style helper or a small module you require at the top of each spec, so the launch logic lives in exactly one place.

How do I capture a screenshot when a test fails?

Read the test outcome in afterEach and save a PNG before teardown, so a headless CI failure leaves you a picture of the page. Vibium's screenshot() returns PNG bytes, so writing it to disk is a single fs call. Jest exposes the current test's state through expect.getState().

const fs = require('fs');
const { browser } = require('vibium');
 
let bro, page;
 
beforeAll(async () => { bro = await browser.launch({ headless: true }); });
afterAll(async () => { await bro.close(); });
beforeEach(async () => { page = await bro.newPage(); });
 
afterEach(async () => {
  const { currentTestName, testPath } = expect.getState();
  // Jest sets the assertion counters; a failed expect leaves them mismatched.
  if (expect.getState().numPassingAsserts === 0) {
    const safe = currentTestName.replace(/\W+/g, '-');
    const png = await page.screenshot({ fullPage: true });
    fs.writeFileSync(`fail-${safe}.png`, png);
  }
  await page.close();
});

For rock-solid pass/fail detection, the most portable pattern is a try/catch inside the test body: run your steps, and on any thrown assertion, screenshot and rethrow.

test('checkout shows the order total', async () => {
  try {
    await page.go('https://store.example.com/checkout');
    expect(await page.find('#total').text()).toContain('$');
  } catch (err) {
    const png = await page.screenshot({ fullPage: true });
    fs.writeFileSync('fail-checkout.png', png);
    throw err; // let Jest still record the failure
  }
});

See take a screenshot for fullPage and clip options. In CI, upload the fail-*.png files as artifacts — the setup mirrors the pipeline in running Vibium in CI/CD with GitHub Actions, which uses pytest but the artifact step is identical for a Jest job.

How do I run the same suite headless in CI?

Read headless mode from an environment variable so one suite runs headed locally and headless on the runner. This is the single switch your CI job flips — there is no separate CI code path to drift out of sync.

const headless = process.env.HEADLESS === 'true';
const bro = await browser.launch({ headless });

A GitHub Actions job then installs Node, installs your deps, and runs Jest with the flag set:

name: Vibium Jest tests
 
on: [push, pull_request]
 
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      HEADLESS: "true"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - name: Install Chrome system libraries
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libnss3 libatk-bridge2.0-0 libgbm1 libasound2 \
            libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libdrm2
      - run: npm ci
      - run: npm test
      - name: Upload failure screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: vibium-screenshots
          path: "fail-*.png"

Because Vibium bundles its BiDi engine and fetches its own Chrome for Testing, the runner needs only Chrome's shared system libraries — no driver to match against the browser version. That removes the single most common cause of red CI in older stacks.

How does Jest parallelism work with Vibium?

Jest runs each test file in its own worker process, so a per-file browser is the natural unit of parallelism. Give every spec file its own beforeAll launch and the workers stay fully isolated — no shared browser, no cross-file interference. Jest sizes the worker pool automatically, and you tune it with --maxWorkers.

# Run up to 4 files in parallel; drop to 2 on a constrained CI runner
npx jest --maxWorkers=4

Because launching Chrome and running several tabs both cost memory, the failure mode to watch for is a runner that runs out of RAM when too many browsers start at once. Two levers keep that in check:

LeverWhat it doesWhen to reach for it
--maxWorkers=NCaps concurrent test files (browsers)CI runners with limited RAM; flaky OOM crashes
Contexts over pagesMany isolated contexts share one browser processHigh test count within a single file
--runInBandForces everything onto one processDebugging a race; small suites

Prefer isolating with newContext() inside a file and letting Jest parallelize across files. That gives you both axes of speed — parallel files and cheap per-test isolation — without spawning a browser per test. For a deeper treatment of throughput and context reuse, see parallelize Vibium tests.

How do I debug a Vibium Jest test locally?

Run headed and target the one test you care about, so you watch the browser do exactly what the failing assertion sees. Jest's -t flag filters by test name, and dropping headless (or setting HEADLESS=false) makes Chrome visible.

# Run only tests whose name matches "checkout", headed, single process
HEADLESS=false npx jest -t "checkout" --runInBand

Jest's --watch mode pairs well with Vibium during development: save the spec and only the affected test re-runs, headed, in seconds. Two more tactics speed up diagnosis:

  • Log page state at the point of failure. Print await page.url() and await page.find('h1').text() right before the failing expect to see where the browser actually landed.
  • Slow nothing down artificially. Resist adding sleep() to "make it pass" — if a step is genuinely slow, raise that action's timeout (await el.click({ timeout: 60000 })) rather than the whole suite's.

Keep --runInBand for debugging only. It serializes every file onto one worker, which is great for stepping through a race but throws away the parallelism you want in CI.

How do I keep a Vibium Jest suite from going flaky?

Trust auto-waiting, select by intent, and isolate state — the same three habits that keep any Vibium suite stable apply directly under Jest. Flake is non-determinism, and the three usual culprits each have a fix.

Flake sourceSymptom under JestFix
Hardcoded sleep/setTimeoutPasses fast locally, times out in CIDelete delays; let find() auto-wait for actionability
Positional selectors (div > button:nth-child(4))Breaks on any UI refactorUse find({ role, text }), find({ label }), or testid
Shared login/state between testsTest only passes after another runs firstFresh newContext() in beforeEach, close() in afterEach
Real time / real networkFlaky countdowns, slow stagingFreeze the clock, mock routes with page.route()

Prefer semantic locators so a CSS change does not break the suite:

// Stable: tied to what the element is, not where it sits
await page.find({ testid: 'checkout' }).click();
await page.find({ role: 'button', text: 'Place order' }).click();
await page.find({ label: 'Coupon code' }).fill('SAVE10');

Vibium's find() takes a CSS string for the common case and a structured object for semantic strategies — role, text, label, placeholder, testid — and combines them in one call, so find({ role: 'button', text: 'Submit' }) needs no chaining. See find an element for the full strategy list and write flake-free Vibium tests for the deeper playbook.

Can I use Vibium's AI checks inside a Jest assertion?

Yes — page.check() runs a plain-English verification and returns a structured result you assert on with expect. This is Vibium's signature feature: instead of reading a specific attribute, you describe the intent and let the AI judge the page from a screenshot plus DOM.

test('the cart is empty on first load', async () => {
  await page.go('https://store.example.com');
 
  const result = await page.check('the shopping cart shows 0 items');
  expect(result.passed).toBe(true);
});

Use AI checks where a claim is easier to describe than to locate — "prices are sorted low to high", "a validation error is shown for the email field", "dark mode is active". They complement, not replace, the deterministic API: keep expect(await el.text()).toBe(...) for exact values, and reach for check() for holistic, human-style assertions. Both live in the same test file and the same expect.

Why is Vibium a clean fit for Jest?

Three design choices make Vibium Jest suites low-maintenance. First, the single Go binary plus auto-downloaded Chrome means zero driver management — no webdriver version to align, ever. Second, headless is a launch flag rather than a separate build, so the suite you debug headed is byte-for-byte the suite CI runs headless. Third, auto-waiting find() removes the timing guesswork that forces sleep() calls into other frameworks, so tests stay green on slow shared runners. Layer Jest's familiar beforeAll/afterEach/expect on top and you get a browser suite that reads like the unit tests your team already writes. Want an AI agent to drive the same browser interactively? See Vibium MCP in Claude Code.

Next steps

Frequently asked questions

How do I use Vibium with Jest?

Install both with npm, import Vibium's sync client inside your test file, and launch a browser in a beforeAll hook. Create a fresh page or context per test, run your find/click/fill steps, assert with expect, and close the browser in afterAll. No extra Jest plugin is required.

Do I need a special Jest preset for Vibium?

No. Vibium is a plain npm package, so it works with a stock Jest install. You only need to raise Jest's default timeout because real browser steps take longer than unit tests. Set testTimeout in jest.config.js or pass a timeout to individual tests.

Should I use the sync or async Vibium API with Jest?

Either works, but the async API is the natural fit because Jest already understands promises. Use require('vibium') and await each call, or use require('vibium/sync') for straight-line scripts. Mixing styles in one suite is discouraged; pick one per project.

How do I take a screenshot when a Jest test fails?

Wrap the body in try/catch, or read the test outcome in afterEach, and call page.screenshot() before rethrowing. Vibium returns PNG bytes, so you write them to disk with fs.writeFileSync. Upload the file as a CI artifact to debug headless failures.

How do I run Vibium Jest tests headless in CI?

Read a HEADLESS environment variable in your launch call and pass headless: true when it is set. The same suite then runs headed on your laptop and headless on the runner. Vibium auto-downloads Chrome for Testing, so there is no driver to install.

How do I isolate state between Jest tests in Vibium?

Create a new browser context inside beforeEach and close it in afterEach. Each context has its own cookies and storage, so a logged-in session from one test never leaks into the next. One launched browser can serve many isolated contexts cheaply.

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

Related guides