CARLOS rastrillo docs

๐Ÿค– harness

amadan.net/rastrillo/rastrillo/harness

A browser-drive rig: a real app on a real localhost origin, driven by a real Chromium with a CDP virtual authenticator attached, for the tests that a Go-only suite cannot reach โ€” a passkey ceremony, a JS-enhanced form, anything that needs an actual browser to be honest about.

Every file but this package's doc comment is built only under -tags browser, so chromedp and its CDP dependencies stay out of the plain build graph: go build ./... and go test ./... never pull them in, and only go test -tags browser ./... does.

go test -tags browser ./...

The origin chicken-and-egg

A passkey app needs its origin before it can even build its handler โ€” CSRF and WebAuthn config both read it โ€” but the origin doesn't exist until a port is bound. New resolves the order: bind a localhost listener first, compute http://localhost:PORT from it, hand that origin to your build function, serve on the listener, and only then launch Chromium.

The origin is always localhost, never a bare IP: WebAuthn's relying party ID has to be a registrable domain, and an IP address is not one. Navigate Rig.Origin, not the listener's address.

type Rig struct {
	Origin string
}

func New(t *testing.T, build func(origin string) http.Handler, opts ...Option) *Rig

New also attaches a CDP virtual authenticator to the browser before handing back the rig, so any WebAuthn ceremony the app drives has somewhere to land โ€” resident keys, user verification and PRF are all enabled on it. Cleanups are registered browser-after-server, so t.Cleanup's LIFO order tears the browser down first, before the server it was talking to.

Options

type Option func(*config)

func WithoutPRFAtCreation() Option
func WithScrollbars() Option

New takes a variadic list of Option values to adjust what it builds.

WithoutPRFAtCreation rehearses the browsers that refuse to return PRF (hmac-secret) output during creation, forcing webauthn.mjs's two-prompt fallback: register() gets an empty extension result from create() and runs an immediate assertion to fetch PRF instead. The CDP virtual authenticator can't withhold PRF at creation on its own โ€” HasPrf is all-or-nothing โ€” so the rig fakes the condition one level up: a script registered on new documents, in the main world, before New ever navigates, wraps navigator.credentials.create and defines an own property getClientExtensionResults: () => ({}) on the credential it returns. credentials.get is left untouched, so the fallback assertion still gets real PRF from the authenticator.

WithScrollbars leaves the browser's scrollbars where the platform draws them. chromedp's headless defaults hide them (--hide-scrollbars, copied from Puppeteer), which is invisible and harmless right up until a drive's subject is the scrollbar: a browser that draws none takes no width out of the layout, so a drive measuring whether a scrollbar shifts the page would measure zero either way and agree with whatever the stylesheet claimed. ui's scrollbar-gutter drive is that drive, and it fails rather than passes if this option stops working โ€” its control page asks for the shift and requires to see it.

The own-property shape matters. Patching PublicKeyCredential.prototype.getClientExtensionResults would strip PRF from the fallback's own assertion too, since assertions read the same prototype method. Wrapping the credential in a Proxy trips its brand-checked response/rawId getters with "Illegal invocation".

Finding Chromium

func ChromePath(t *testing.T) string

ChromePath locates the Chromium a browser-tagged test drives, in order: the RASTRILLO_CHROME environment variable, then chromium, chromium-browser, google-chrome and google-chrome-stable on PATH, then the Playwright cache (~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome).

A skip is not a pass: with no browser found, ChromePath fails the test outright, unless RASTRILLO_BROWSER_OPTIONAL is set โ€” which turns that failure into a deliberate, visible skip instead of a silent gap in coverage. New calls ChromePath itself, so a rig-based test gets this behavior for free.

Driving the browser

func (r *Rig) Context() context.Context
func (r *Rig) Run(actions ...chromedp.Action)

Run executes a script of plain chromedp actions against the rig's browser. There is no extra DSL on top of chromedp's own.

On failure it fails the test immediately, and the message includes whatever was on screen at the time. A failing drive reports what a person looking at the browser would have seen, which is usually more useful than the error chromedp returned.

Context exposes the rig's underlying chromedp context directly, for the rare drive that needs a tighter deadline than the test binary's own timeout affords.

The loud-failure watchers

func (r *Rig) Allow(method, path string, status int)
func (r *Rig) Screen(selector, note string)

New wires up watchers before it hands back the rig, so a silent failure is impossible. A console error or assertion, an uncaught exception, a failed request, and any response with status >= 400 are all recorded as problems. A 4xx/5xx response also shows up a second time as Chromium's own console-error mirror of it โ€” that mirror arrives over the CDP log domain rather than the console-API event, and carries only a URL, not a method or status.

Screen(selector, note) is the gate a drive passes at every screen boundary: it waits for selector to become visible, then flushes the accumulated problems, failing the test โ€” naming note and whatever was on screen โ€” if any turned up. "body" is the whole-page case for rastrillo's server-rendered apps, which have no #app convention to hard-fail on.

Some probes are expected. A signed-out boot asking /api/me and being told 401 is how your app finds out to show the sign-in screen. Allow(method, path, status) excuses exactly that response, matched by path, and its console-error mirror along with it โ€” matched by path alone, since the mirror carries no method or status. New calls Allow(http.MethodGet, "/favicon.ico", http.StatusNotFound) itself, so the browser's own favicon probe never needs rediscovering by every app that uses the rig.

The junk scan

func (r *Rig) AllowText(s string)

Screen doesn't just wait for its selector and flush the problem list โ€” between the two it scans the screen for the values that render perfectly and say nothing: "undefined", "null", "[object Object]" and "NaN", wherever a template silently dropped a field or mishandled a shape. The scan reads the screen the way a person would (the root's textContent, rooted at selector) plus the places a person cannot see โ€” every input/textarea value and every [aria-label] a screen reader would announce. Any hit fails the test, naming the note and quoting the surrounding text so a substring like "null" is legible in context rather than reported bare.

That context matters because "null" is also honest English โ€” "this contract is null and void" โ€” so AllowText(s) exempts one exact string from the scan for the rest of that test. The allowance is the surrounding text, not the junk value: everything else on the screen, including other occurrences of "null", is still scanned.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.