From biloba
Writes and reviews Biloba browser tests using Ginkgo/Gomega, covering the dual immediate/matcher API, element selection (CSS, XPath, text), readiness anchors, request stubbing, and multi-tab flows.
How this skill is triggered — by the user, by Claude, or both
Slash command
/biloba:write-testsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Assumes Biloba is already wired into the suite (`biloba:setup`) and you know the principles (`biloba:overview`). For the full method list see `biloba:api`; for XPath see `biloba:xpath`. Docs: <https://onsi.github.io/biloba/#working-with-the-dom>.
Assumes Biloba is already wired into the suite (biloba:setup) and you know the principles (biloba:overview). For the full method list see biloba:api; for XPath see biloba:xpath. Docs: https://onsi.github.io/biloba/#working-with-the-dom.
Most DOM methods have two forms keyed on argument count:
b.Click("#go")
b.SetValue("#name", "Jane")
text := b.InnerText("#title")
Eventually("#go").Should(b.Click()) // poll until clickable, then click once
Eventually("#name").Should(b.SetValue("Jane")) // poll until settable
Eventually("#title").Should(b.HaveInnerText("Welcome"))
The matcher form lets you fold readiness-waiting into the action — no separate "is it there yet" poll. b.Click("#login") right after b.Navigate may race the page load; Eventually("#login").Should(b.Click()) won't.
First-vs-all naming. A bare method acts on the first match; the ForEach/Each sibling acts on all matches (returning/asserting slices, empty when nothing matches): InnerText vs InnerTextForEach/EachHaveInnerText; GetProperty vs GetPropertyForEach/EachHaveProperty; Click vs ClickEach. The name tells you which.
Navigate, gate on a readiness anchor, then exercise behavior:
var _ = Describe("the search page", func() {
BeforeEach(func() {
b.Navigate("http://localhost:8080/search")
Eventually("#results").Should(b.Exist()) // page is ready once this appears
})
It("finds matches", func() {
b.SetValue("#q", "biloba")
Eventually(b.WithText("Search")).Should(b.Click())
Eventually(".result").Should(b.HaveCount(BeNumerically(">", 0)))
})
})
b.Navigate(url) also asserts the response was 200 (use NavigateWithStatus for other codes).b.Exist() or b.BeVisible().HaveInnerText/HaveText), counts (HaveCount), URL/title (HaveURL/HaveTitle), or network effects (HaveMadeRequest).A selector is either a CSS string or an XPath value:
b.Click("button.submit") // CSS — first matching element
b.Click(b.XPath("button").WithText("OK")) // XPath via the DSL → biloba:xpath
b.Click(b.WithText("Submit")) // sugar for b.XPath().WithText(...) — any element by exact text
b.Click(b.WithTextContains("Sub")) // ...by substring
Prefer text/role selectors for things a user names by label; fall back to ids/data-* for the rest. Never fetch-then-act — always pass the selector into the action so find-and-act is one atomic JS snippet.
Piercing shadow DOM / iframes with the CSS-only >>> combinator (one boundary per >>>, open shadow roots and same-origin iframes only):
b.Click("my-widget >>> button.submit")
Eventually("#editor-frame >>> .toolbar .save").Should(b.Click())
Favor testing against real backends whenever possible and focus on fixing flakes and performance there. But, if you must stub, stub the endpoints you don't want to depend on; everything unmatched passes through to the real network (#stubbing-and-observing-the-network):
b.StubRequest(ContainSubstring("/api/users"), biloba.StubResponse{
Body: `[{"name":"Jane"},{"name":"Bob"}]`,
Headers: map[string]string{"Content-Type": "application/json"},
})
b.Navigate("/app")
Eventually(".user").Should(b.HaveCount(2))
Stubs are per-tab and reset by Prepare(). Observe requests with Eventually(b).Should(b.HaveMadeRequest(...)) and wait for quiet with Eventually(b).Should(b.BeNetworkIdle()).
Set an auth cookie or localStorage to jump past login (navigate to a real origin first — about:blank can't hold cookies/storage):
b.Navigate("/home")
b.SetCookie(biloba.Cookie{Name: "user", Value: "Joe"})
DeferCleanup(b.ClearCookies)
Or shortcut straight through your app's JS API (#running-arbitrary-javascript):
b.Run(`app.load(` + jsonFixture + `); app.redraw()`)
Eventually("#doc-name").Should(b.HaveInnerText("My Fixture Data"))
b.Run is synchronous; use b.RunAsync (which returns an awaited value) for fetch/await. b.EvaluateTo asserts on a JS expression directly.
tab := b.NewTab() // isolated, incognito-like context; closed by Prepare()
login(b, "sally"); login(tab, "jane")
Eventually(userXPath.WithText("Jane")).Should(b.HaveClass("online"))
Tabs opened by the page (e.g. target="_blank") are spawned tabs — find them with the HaveSpawnedTab/AllSpawnedTabs (or HaveTab/AllTabs) queries:
tab.Click(linkXPath)
Eventually(tab).Should(tab.HaveSpawnedTab().WithURL("https://youtube.com/..."))
yt := tab.AllSpawnedTabs().Find(tab.TabMatching().WithURL("https://youtube.com/..."))
A DOM method always operates on the tab it's invoked on (tab.Click, not b.Click). Dialogs and downloads are per-tab too — register dialog handlers before the action that triggers them.
Drop to chromedp via b.Context (real :hover, cross-origin frames, geolocation, anything CDP). See the escape hatch in biloba:overview. For real keystrokes use b.Type/b.SendKeys rather than SetValue.
Propose opening an issue if a common pattern is missing.
npx claudepluginhub onsi/biloba --plugin bilobaExplains the Biloba mental model for writing browser tests with Ginkgo/Gomega: performance via parallelization, stability via pragmatic simulation, conciseness, and the chromedp escape hatch.
Guides creation of Browser Library tests using Playwright-powered automation for web testing, covering locators, auto-waiting, assertions, iframes, Shadow DOM, and multi-tabs.
Explains the Ginkgo mental model for writing Go tests: tree construction then running, spec independence, and node taxonomy. Use this first when working with Ginkgo.