Skip to main content

write-tests

Author Biloba specs in a Go Ginkgo/Gomega suite — the dual immediate/matcher API (act now vs. return a matcher you poll with Eventually), capturing a matcher's observed value with .Capture instead of asserting-then-re-reading, first-vs-all naming, the navigate-then-readiness-anchor shape (gate on the DOM, then read GetLocation), selecting elements (CSS targeting stable hooks as the default, semantic role/text/label locators, anchoring a locator scope so a negative assertion isn't vacuous, the >>> piercing combinator, XPath), the interaction vocabulary (click variants, drag, scroll, tap, text selection), realistic mode for occlusion/hover smoke tests, visual regression with b.HaveScreenshot against a committed baseline, hermetic tests via request stubbing/aborting/modifying/holding, the GetJSValue app-state barrier (and when it gates nothing), multi-tab flows, cross-origin iframes (b.Frame frame handles), and seeding state. Use when writing or reviewing Biloba browser tests.

ソース情報

リポジトリ
onsi/biloba
ソースの最終更新活動
2026年9月15日 14:37
検出された SKILL.md の言語
英語
スター
29
フォーク
9

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
write-tests
description
Author Biloba specs in a Go Ginkgo/Gomega suite — the dual immediate/matcher API (act now vs. return a matcher you poll with Eventually), capturing a matcher's observed value with .Capture instead of asserting-then-re-reading, first-vs-all naming, the navigate-then-readiness-anchor shape (gate on the DOM, then read GetLocation), selecting elements (CSS targeting stable hooks as the default, semantic role/text/label locators, anchoring a locator scope so a negative assertion isn't vacuous, the >>> piercing combinator, XPath), the interaction vocabulary (click variants, drag, scroll, tap, text selection), realistic mode for occlusion/hover smoke tests, visual regression with b.HaveScreenshot against a committed baseline, hermetic tests via request stubbing/aborting/modifying/holding, the GetJSValue app-state barrier (and when it gates nothing), multi-tab flows, cross-origin iframes (b.Frame frame handles), and seeding state. Use when writing or reviewing Biloba browser tests.
# Writing Biloba specs Assume a **Go/Ginkgo** suite, wired with `setup`, and apply the principles in `overview`. Method list → `api`. XPath → `xpath`. Flakes → `flaky-specs`. Docs: <https://onsi.github.io/biloba/#working-with-the-dom>. Sibling skills are named here without a prefix; invoke one with the same plugin prefix you loaded this skill under. ## RULE — get these two right in the first draft 1. **Selecting elements.** User-facing things → a **Locator** by role/name/text (`b.ByRole("button").WithName("Save")`, `b.ByText("Sign in")`), which doubles as an a11y guard. Structural/state hooks you own → **CSS on a stable `#id`/`[data-testid]`**, never a styling class. 2. **Assert observable outcomes** — visible text, counts, URL/title, network effects — not internal class/structure. **Smells to catch in your own draft** (generic-automation muscle memory gets these wrong): | Smell | Instead | |---|---| | `:nth-of-type`, `.btn-primary` | `b.ByCSS(sel).Nth(i)`, or a stable `#id`/`[data-testid]` | | text-matching XPath | `b.ByText(...)` / `b.ByRole(...).WithName(...)` | | `b.Run("...querySelectorAll(s).length")` | `b.HaveCount(...)` — reinventing a matcher | | IIFE-wrapping a script to get `return` | `b.RunAsync` | | `SetValue` when you meant keystrokes | `b.Type` | | `b.Run(expr, &x)` then `Expect(x)` — single-shot read | `Eventually(b.Run).WithArguments(expr)` | | matcher assertion, then a getter for the same value | `.Capture(&x)` on the matcher — one read | | a guarded/conditional interaction ("click X only if…") | fix the spec's state determinism upstream → `flaky-specs` | ## The one pattern: dual, and poll-by-default Most DOM methods have **two forms keyed on argument count**. ```go // Fully applied → POLLS until ready, acts/reads exactly once, stops. The everyday form. b.Click("#go") // polls until clickable, clicks once b.SetValue("#name", "Jane") // polls until settable, sets once text := b.GetInnerText("#title") // polls until present, reads once // Under-applied → returns a matcher YOU poll. For a custom timeout, a Consistently, or .And(). Eventually("#go").Should(b.Click()) Eventually("#name").Should(b.SetValue("Jane")) Eventually("#title").Should(b.HaveInnerText("Welcome")) ``` A fully-applied action dispatches **once** on the first success and stops — safe even on a **toggle**, no oscillation, no "fired a frame too early" race. **Tune or opt out** with shallow `*Biloba` clones: `b.WithTimeout(5*time.Second).Click("#go")`, `b.WithPolling(d)`, `b.WithContext(ctx)`. `b.Immediate()` reverts to act-once/fail-fast — **you almost never want it**. Misapplying a knob (e.g. `WithPolling` on a snapshot, or any knob on the bare matcher form) is a hard error → four-bucket table in `api`. **First-vs-all naming.** A bare method acts on the **first** match. Plural getters are **snapshots** (no poll — gate presence first); plural actions carry an **`Immediately` suffix** (act now, no poll; the long suffix is a deliberate "know what you're doing" smell). Matchers over all matches are `Each*` and **fail on zero matches**. | first (polls) | all (snapshot / immediate) | matcher over all | |---|---|---| | `GetInnerText` | `CurrentInnerTextForEach` | `EachHaveInnerText` | | `GetProperty` | `CurrentPropertyForEach` | `EachHaveProperty` | | `Click` | `ClickEachImmediately` | — | `EachHaveInnerText`/`EachHaveTextContent`'s sub-matcher and `.Capture` see a typed `[]string` (same slice their `Current*ForEach` getter returns) — `Equal([]string{...})` works. The generic `EachHaveProperty` hands over the raw JSON-decoded `[]any` (properties are heterogeneous) — `Equal([]string{...})` always fails there; reach for `HaveExactElements(...)`. Same shape as `EvaluateTo`'s raw-vs-typed asymmetry, below. ## The spec shape ```go var _ = Describe("the search page", func() { BeforeEach(func() { b.Navigate("http://localhost:8080/search") Eventually("#results").Should(b.Exist()) // readiness anchor }) It("finds matches", func() { b.SetValue("#q", "biloba") b.Click(b.ByRole("button").WithName("Search")) Eventually(".result").Should(b.HaveCount(BeNumerically(">", 0))) }) }) ``` - `b.Navigate(url)` asserts the response was `200` (`NavigateWithStatus` for other codes). - Pick a **stable, meaningful** anchor (a heading, a key container) with `b.Exist()`/`b.BeVisible()`. **For an in-app navigation, gate on a DOM anchor and *then* read the location — don't poll the URL.** The URL flips the instant the router pushes it, so it's the weakest proof the destination rendered. ```go b.Click("#submit") Eventually("#dashboard-root").Should(b.Exist()) // proves the navigation landed… Ω(b.GetLocation()).Should(HaveSuffix("/dashboard")) // …so this is a formality ``` `b.GetLocation()`/`b.GetTitle()` are polling getters honoring all four knobs, and a transient CDP error mid-navigation (`Inspected target navigated or closed`) is a retryable miss — so `Eventually(b.GetLocation)` is safe, just weaker. ## Pocket matcher cheat-sheet — reach here before `b.Run` | Want to assert… | Matcher | |---|---| | present / visible | `b.Exist()` / `b.BeVisible()` | | how many match | `b.HaveCount(BeNumerically(">", 0))`; distinct by key: `b.HaveDistinctCount("data-key", 3)` | | visible text | `b.HaveInnerText("…")` / `b.HaveTextContent(…)` / `b.HaveText(…)` (whitespace-collapsed) | | a DOM/JS property | `b.HaveProperty("href", …)` / `b.HaveClass("active")`; JSON attr `b.HaveJSONAttribute("data-state", HaveKeyWithValue(…))` (getter `b.GetJSONAttribute(sel, attr, &out)`) | | actually clickable (visible+enabled+topmost) | `b.BeClickable()` | | form value | `b.HaveValue(…)` | | tab URL / title / spawned tab | `b.HaveURL(…)` / `b.HaveTitle(…)` / `b.HaveSpawnedTab(…)`; getters `b.GetLocation()`/`b.GetTitle()` | | the app applied a server response | `b.GetJSValue("window.__storeLog", &log)` — app-state barrier (below); *not* the DOM, *not* a Go-side HTTP read | | a network request was made | `Eventually(b).Should(b.HaveMadeRequest(…))` | | layout / box / scroll | `b.HaveBoundingBox(HaveField("Top", …))` / `b.HaveOffsetTopWithin(container, …)` / `b.HaveScrollOffset(…)`. `Box.Width`/`Height` = border-box; `ClientWidth`/`ClientHeight` = scrollbar-excluded content box | | A relative to B | `b.BeAbove` / `BeBelow` / `BeLeftOf` / `BeRightOf` / `b.Encloses` / `b.Overlaps`; numeric `b.GetGapBetween(a,b)` → `BoxDelta` | | on screen / document order | `b.BeInViewport()` (partial; `b.BeInViewport(b.Fully())` = whole box) / `b.BePrecededBy` / `b.BeFollowedBy` | | computed style | `b.HaveComputedStyle(prop, …)` / `b.HaveComputedStyleNumeric(prop, …)`; color across syntaxes `b.HaveComputedStyle(prop, b.MatchColor("var(--tok)"))`; getters `b.GetComputedStyle`/`GetComputedStyleNumeric`/`NormalizeColor` | | scroll a target into view (instant) | `b.ScrollIntoView(sel)` — options `b.WithinScroller(container)`, `b.AtTopOffset(px)` | | an arbitrary JS expression | `Eventually(expr).Should(b.EvaluateTo(matcher))` | | it still *looks* like this | `b.HaveScreenshot("name")` against a committed baseline → `visual-assertions` | - `EvaluateTo`/`Run` decode JS numbers to **float64** — `BeNumerically("==", n)`, not `Equal(intLiteral)`. - **`BePrecededBy`/`BeFollowedBy` read subject-first**: `Eventually(X).Should(b.BeFollowedBy(Y))` ⇔ X comes **BEFORE** Y. A backwards one passes silently — also assert the inverse doesn't hold. - **Geometry matchers error on a missing element**, so `Eventually(sel).ShouldNot(b.BeInViewport())` times out. Assert disappearance with `ShouldNot(b.Exist())` / `Should(b.HaveCount(0))`. ## `.Capture(&x)` — don't re-read what you just asserted on ```go var blockID string Eventually(".figure-frame").Should(b.HaveAttribute("data-block-id", Not(BeEmpty())).Capture(&blockID)) b.Click("#block-" + blockID) ``` Asserting and *then* calling the getter is two reads of a page that may have changed in between. `Capture` hands you the value from the winning read, decoded into your Go type, and writes only on a match (`ShouldNot` captures nothing). Value-less matchers (`Exist`, `BeVisible`, `BeInViewport`, the relational ones) and actions-as-matchers deliberately don't compile with it. Decode rules (typed-decode mismatches, absent-vs-zero via `**T`, the new-matcher-each-time behavior, narrowing a JS object vs. a Biloba struct) → `flaky-specs` §1. Full capturable list → `api`. ## Selecting elements — the vocabulary A `selector` is a **CSS string** (fastest; `:has()`; `>>>` pierces shadow/iframe), a **`Locator`** (`b.By*`; most resilient, slowest — full-document ARIA scan), or an **`XPath`** (`xpath`; axis/ordinal; pierces neither). ```go b.Click("#go") // CSS by id — the preferred default b.Click("[data-testid=save]") // CSS by intentional test-id Eventually("tr:has(td.overdue)").Should(b.Exist()) // CSS :has() — "the row that contains X" b.Click(b.ByRole("button").WithName("Save")) // Locator — role + accessible name (a11y guard) b.Click(b.ByText("Submit")) // Locator — visible text (ByTextContains for substring) b.SetValue(b.ByLabel("Email"), "jane@acme.com") // Locator — a control by its label b.Click(b.XPath("li").WithText("OK").Ancestor("ul"))// XPath — an axis query nothing else expresses ``` Constructors (each text-valued one has a `*Contains` variant): `b.ByRole`, `b.ByText`, `b.ByLabel`, `b.ByPlaceholder`, `b.ByAltText`, `b.ByTitle`, `b.ByTestID` (attr = `biloba.TestIDAttribute`, default `data-testid`), `b.ByCSS(sel)` (raw CSS into the algebra — the structural escape hatch). Refine a role with `.WithName(n)`, `.Level(n)`, or ARIA states `.Checked()`/`.Disabled()`/`.Expanded()`/`.Pressed()`/`.Selected()`. Locators **compose**, and every filter accepts **any** selector (CSS/XPath/Locator): ```go b.ByRole("listitem").ContainingText("Product 2") // .ContainingText / .NotContainingText b.ByRole("listitem").Containing(b.ByText("Delete")) // .Containing / .NotContaining (a descendant) b.ByRole("button").And(".primary") // .And / .Or — intersection / union b.ByRole("button").WithName("Delete").Within("#dialog") // .Within / .NotWithin b.ByText("Item").Nth(2) // .Nth(i) / .First() / .Last() b.ByCSS(".story").Nth(1) // the 2nd .story ``` **Trap — an unresolved `.Within`/`.Containing` scope matches nothing, so a negative assertion is vacuous** — it passes instantly and forever if the scope never renders. Anchor the scope first: ```go Eventually("#published-list").Should(b.Exist()) // the scope is real…
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る