--- name: geb-spock description: > Write Geb browser automation tests using the Spock framework with best practices. Use this skill whenever the user asks to write, generate, fix, or review Geb test specs, mentions Geb, GebSpec, GebReportingSpec, or asks about browser automation with Groovy/Spock. Also trigger for requests involving Page Objects in a Groovy/Gradle project, `at` checkers, Geb content DSL, or `waitFor` usage. Even if the user just says "write me a Geb test" or "help me test this page", use this skill. license: Apache-2.0 --- # Geb Testing Skill Generates and reviews Geb + Spock browser automation specs following intermediate-level best practices. Geb documentation is at https://groovy.apache.org/geb/manual/current/. ## Core Principles 1. **Always use Page Objects** — never inline selectors in specs. 2. **Use `at` checkers** — every Page Object must declare `static at`. 3. **Prefer CSS selectors** over XPath; avoid brittle nth-child selectors. 4. **Use `waitFor` explicitly** for dynamic content; never `Thread.sleep`. 1. Strongly prefer to put `waitFor` method calls into methods inside classes that extend `geb.Page`, `geb.Module`, or other shared test fixtures. If clicking or interacting with some object on a page requires waiting for some response, it's best to encapsulate that knowledge into a method on the pageObject (e.g. `deleteTableRow` or `submitForm`) that then waits for the signal that the operation is complete. That will make the knowledge of your page more reusable and avoid repeated `waitFor`. 5. **Use `GebReportingSpec`** (not `GebSpec`) so screenshots are captured on failure. 6. **Keep specs in `given/when/then` blocks** — one behaviour per feature method. 1. Caveat: if a behaviour requires multiple steps, it's okay to have multiple `when/then` block pairs within one test method, so long as each intermediary step is integral to the journey under test. 7. **Use Modules** for repeated UI components (nav bars, modals, form rows). 8. Avoid writing flaky tests by stress-testing your Geb spec before committing: 1. Try [introducing network latency](https://groovy.apache.org/geb/manual/current/#controlling-network-conditions) to verify that a test will pass on a slow network. 2. Try re-running tests many times in succession locally to ensure they aren't flaky --- ## Spec File Structure ```groovy package com.example.specs import geb.spock.GebReportingSpec import com.example.pages.LoginPage import com.example.pages.DashboardPage import spock.lang.Stepwise // only when steps truly depend on each other @Stepwise // omit if steps are independent class LoginSpec extends GebReportingSpec { def "successful login redirects to dashboard"() { given: "user is on the login page" to LoginPage when: "valid credentials are submitted" loginForm.username = "user@example.com" loginForm.password = "secret" loginForm.submit() then: "the dashboard is displayed" at DashboardPage welcomeMessage.text() == "Welcome, user@example.com" } def "login fails with invalid credentials"() { given: to LoginPage when: loginForm.username = "bad@example.com" loginForm.password = "wrong" loginForm.submit() then: at LoginPage errorMessage.displayed errorMessage.text().contains("Invalid credentials") } } ``` --- ## Page Object Structure ```groovy package com.example.pages import geb.Page import com.example.modules.LoginFormModule class LoginPage extends Page { // `at` checker — verified when `to` or `at` is called static at = { title == "Login | MyApp" } // URL for `to LoginPage` static url = "/login" static content = { // Lazy by default; set required: false for optional elements loginForm { module(LoginFormModule) } errorMessage(required: false) { $(".alert-error") } } } ``` ```groovy class DashboardPage extends Page { static at = { $("h1.dashboard-title").displayed } static url = "/dashboard" static content = { welcomeMessage { $(".welcome-msg") } navBar { module(NavBarModule) } } } ``` --- ## Module Structure Use modules for components that repeat across pages (nav, modal, form row, table row). ```groovy package com.example.modules import geb.Module class LoginFormModule extends Module { static content = { username { $("input[name='username']") } password { $("input[name='password']") } submitBtn { $("button[type='submit']") } } void submit() { submitBtn.click() } } ``` --- ## `waitFor` Patterns ```groovy // Wait for element to appear (default timeout from GebConfig) waitFor { $(".spinner").not(".active") } // Wait with custom timeout (seconds) waitFor(10) { successBanner.displayed } // Wait and return the element def result = waitFor { $(".result-row") } // Use in content DSL for elements that load dynamically static content = { // Wait for this element every time it's accessed asyncTable(wait: true) { $("table.results") } } ``` > **Never use `Thread.sleep`**. If `waitFor` keeps timing out, make sure your assertion is actually eventually true (you may be relying on an unreliable side effect), ensure that your page is completing its work as quickly as it can, or increase the timeout in `GebConfig.groovy` rather than sleeping. --- ## Selector Best Practices | ✅ Prefer | ❌ Avoid | |-------------------------------|--------------------------------------------------| | `$("input[name='email']")` | `$("div > div:nth-child(2) > input")` | | `$(".submit-btn")` | `$("button", text: "Submit")` (fragile for i18n) | | `$("table.results tbody tr")` | XPath selectors | | `data-testid` attributes | Position-based selectors | If the app doesn't have `data-testid` attrs, use stable semantic selectors: `name`, `id`, `aria-label`, `role`. --- ## Common Patterns ### Checking a list of items ```groovy def rows = rows.size() == 3 rows[0].find("td.name").text() == "Alice" ``` ### Filling a form ```groovy $("input[name='firstName']").value("Jane") $("select[name='country']").value("US") $("input[type='checkbox'][name='agree']").value(true) ``` ### Interacting with dropdowns (non-native) ```groovy // For custom JS dropdowns (not