FrontierJS

Mesa guide / Reference

@frontierjs/mesa/drive

A real Chrome, driven over the DevTools protocol, for your app's own end-to-end tests. It has no dependencies: the protocol runs over the global WebSocket. For how to write a drive, see Testing.

import { findChrome, openChrome, createNetwork, PAGE_AUDIT } from '@frontierjs/mesa/drive'

Types ship in the package.

findChrome

findChrome(): string | null

The Chrome binary, or null. It is $FJS_CHROME when that is set (and a variable that points at nothing finds nothing), otherwise the first of google-chrome, google-chrome-stable, chromium and chromium-browser on PATH, then the macOS app bundles. Ask it first when a drive should skip on a machine with no Chrome. openChrome throws instead.

openChrome

openChrome(options?: ChromeOptions): Promise<Chrome>

Launches Chrome, attaches to one page and answers the handle. The caller closes it. A failed launch throws a sentence that names the fix and never exits your process.

OptionDefaultEffect
profilea temporary directory, removed on closeA directory you keep. IndexedDB, Cache Storage, the service worker and OPFS live in it
bootstrapnoneA script run before anything else in every document
windowSize'1280,900''width,height'
args[]More Chrome flags. --remote-debugging-port and --user-data-dir are refused
headedfalseA real window on $DISPLAY, for what only a headed browser can do
clockofftrue or { at: epochMs }. The first page holds its time still until the drive moves it

The handle

MemberDoes
navigate(url, ready?)Loads url as a new document. ready is an expression polled until it is truthy
evaluate(body)Runs the body of an async function in the page. return answers it. Never return a bare null; wrap it in an object
waitFor(expr, { ms, every, label })Polls until expr is truthy and answers its value
clickAt(selector)A trusted click at the element's center
hoverAt(selector)Moves the pointer onto the element
fill(selector, text)Replaces a field's text through the keyboard
type(text)Types into the focused element
press(key, modifiers?)A key press, such as 'Enter'
key(key, options?)A key press with code, keyCode, modifiers and text set by hand
shot(path)Writes a PNG and answers the path
newPage(url?, ready?) or newPage({ url, ready, clock })A second page with evaluate, navigate, clock and close. Its errors land in the same errors array
errorsA live array of what the page threw, its console.error calls and any [Mesa] warning
failedA live array of '<status> <url>' for each response of 400 and above, the favicon aside
downloadsThe directory every downloaded file lands in. Removed on close
clockMoves a held page clock. See below
on(method, fn)Subscribes to a protocol event. Answers the unsubscribe function
cmd(method, params)A protocol call on the page
send(method, params, sessionId?)A browser-level protocol call
close()Closes Chrome and removes the temporary profile

errors and failed are separate because a signed-out page asking for its account gets a 401 by design. Read errors after every step that can fail: a component that throws mid-render leaves a partial tree, and assertions about what is there still pass.

Focus is emulated. A headless window would blur about thirty seconds in, and after that el.focus() fires no focus event.

The page clock

A page opened with openChrome({ clock: true }) or newPage({ clock: true }) holds its time still. Everything on Mesa's clock ($.after, $.every, $.sleep, debounce, transitions) waits until the drive moves it.

await page.clock()                 // now, in epoch ms
await page.clock(500)              // run every timer due in the next 500 ms
await page.clock('settle')         // run one-shot timers until none is armed
await page.clock('real')           // virtual time follows real time again
await page.clock('hold')           // stop it again

'settle' throws naming what is still armed, since an $.every never settles, unless you pass { allowArmed: true }. A page that holds no clock throws on any clock call.

createNetwork

createNetwork(browser: Chrome): Promise<Network>

Takes the network away, as DevTools' offline switch does, and also closes the sockets the page opened. Call it before the first navigation, so the page's sockets are registered. A Vite HMR socket is left alone.

MemberDoes
goOffline()Goes offline. Answers how many sockets it closed, or null when the page loaded before createNetwork
goOnline()Goes back online
waitOnline({ tries, socket })Waits for the browser, then the app's socket reconnecting. Answers whether it did
withOffline(fn, { sever })Runs fn offline and restores the network even if it throws. { sever: false } leaves sockets open, like a real outage's first seconds
severedHow many sockets the last goOffline closed

The emulation is per page, so a newPage() stays online, and a service worker's own network is not covered.

PAGE_AUDIT

PAGE_AUDIT: string

An expression for evaluate that returns an array of the page-level accessibility facts a machine can settle: lang on <html>, one <main>, one .skip-link that is the first focusable element and points at a focusable target, a name on each <nav> when there are several, one <h1>, and no skipped heading level. An empty array means the page is in order. A page with no .skip-link fails it, so it fits apps that follow that convention, as Sierra's layout does.

const problems = await page.evaluate(`return ${PAGE_AUDIT}`)

See Testing for a worked drive.