Qaskills Geb Testing
Browser automation testing with Geb framework for Groovy/JVM using jQuery-like content DSL, Page Object pattern, Spock integration, and WebDriver abstraction.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/geb-testing" ~/.claude/skills/pramoddutta-qaskills-geb-testing && rm -rf "$T"
manifest:
seed-skills/geb-testing/SKILL.mdsource content
Geb Testing
You are an expert QA engineer specializing in Geb, the Groovy-based browser automation framework built on top of WebDriver. When the user asks you to write, review, debug, or set up Geb tests, follow these detailed instructions. You understand the Geb ecosystem deeply including the jQuery-like content DSL, Page Object pattern with content blocks, module composition, Spock integration, waiting/polling, and configuration management.
Core Principles
- Content DSL — Use Geb's declarative
blocks in Page Objects to define element references. The jQuery-likecontent
navigator provides powerful element selection.$() - Page Object Pattern — Every page interaction goes through a Page class with
definitions. Pages define theirstatic content
,url
checker, and navigable content.at - Module Composition — Extract reusable UI components (headers, footers, modals, tables) into Module classes that can be embedded in multiple pages.
- Implicit Assertions — Geb automatically waits for
checkers to pass during page transitions. Useat
for dynamic content.waitFor {} - Spock Integration — Use
orGebSpec
as the base class for tests. Spock's BDD-styleGebReportingSpec
blocks pair naturally with Geb.given/when/then - Configuration Over Code — Use
for browser selection, base URL, waiting strategies, and reporting. Keep tests focused on behavior.GebConfig.groovy - Navigator API — Master the
navigator for element selection. Chain methods like$()
,.text()
,.value()
,.click()
for fluent interactions..attr()
Project Structure
project-root/ ├── build.gradle # Gradle build with Geb dependencies ├── src/ │ └── test/ │ ├── groovy/ │ │ ├── pages/ │ │ │ ├── BasePage.groovy │ │ │ ├── LoginPage.groovy │ │ │ ├── DashboardPage.groovy │ │ │ └── CartPage.groovy │ │ ├── modules/ │ │ │ ├── NavBarModule.groovy │ │ │ ├── ModalModule.groovy │ │ │ └── TableModule.groovy │ │ ├── specs/ │ │ │ ├── auth/ │ │ │ │ ├── LoginSpec.groovy │ │ │ │ └── RegistrationSpec.groovy │ │ │ ├── shopping/ │ │ │ │ └── CartSpec.groovy │ │ │ └── smoke/ │ │ │ └── SmokeSpec.groovy │ │ └── helpers/ │ │ ├── TestDataHelper.groovy │ │ └── ApiHelper.groovy │ └── resources/ │ └── GebConfig.groovy # Geb configuration ├── reports/ │ └── geb/ └── gradle/ └── wrapper/
Detailed Code Examples
Geb Configuration
// src/test/resources/GebConfig.groovy import org.openqa.selenium.chrome.ChromeDriver import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.firefox.FirefoxDriver waiting { timeout = 10 retryInterval = 0.5 includeCauseInMessage = true } atCheckWaiting = true environments { chrome { driver = { ChromeOptions options = new ChromeOptions() options.addArguments('--window-size=1920,1080') if (System.getenv('CI')) { options.addArguments('--headless', '--no-sandbox', '--disable-dev-shm-usage') } new ChromeDriver(options) } } firefox { driver = { new FirefoxDriver() } } } baseUrl = System.getenv('BASE_URL') ?: 'http://localhost:3000' reportsDir = new File('reports/geb') reportOnTestFailureOnly = false
Page Objects with Content DSL
// src/test/groovy/pages/LoginPage.groovy import geb.Page class LoginPage extends Page { static url = '/login' static at = { title.contains('Login') || emailInput.displayed } static content = { emailInput { $('[data-testid="email-input"]') } passwordInput { $('[data-testid="password-input"]') } loginButton { $('[data-testid="login-submit"]') } errorMessage(required: false) { $('[data-testid="error-message"]') } forgotPasswordLink { $('a', text: 'Forgot password?') } rememberMeCheckbox { $('[data-testid="remember-me"]') } } void login(String email, String password) { emailInput.value(email) passwordInput.value(password) loginButton.click() } String getErrorText() { waitFor { errorMessage.displayed } errorMessage.text() } } // src/test/groovy/pages/DashboardPage.groovy import geb.Page class DashboardPage extends Page { static url = '/dashboard' static at = { welcomeMessage.displayed } static content = { welcomeMessage { $('[data-testid="welcome-message"]') } navBar { module NavBarModule } userMenu { $('[data-testid="user-menu"]') } notificationBadge(required: false) { $('[data-testid="notification-count"]') } recentActivity { $('[data-testid="activity-list"] li') } } String getWelcomeText() { welcomeMessage.text() } int getActivityCount() { recentActivity.size() } void clickUserMenu() { userMenu.click() waitFor { $('[data-testid="user-dropdown"]').displayed } } }
Modules for Reusable Components
// src/test/groovy/modules/NavBarModule.groovy import geb.Module class NavBarModule extends Module { static content = { homeLink { $('nav a', text: 'Home') } productsLink { $('nav a', text: 'Products') } cartLink { $('nav a[href="/cart"]') } cartCount(required: false) { $('[data-testid="cart-count"]') } searchInput { $('nav input[type="search"]') } searchButton { $('nav button[type="submit"]') } } void navigateTo(String section) { $("nav a", text: section).click() } void search(String query) { searchInput.value(query) searchButton.click() } int getCartItemCount() { cartCount.displayed ? cartCount.text().toInteger() : 0 } } // src/test/groovy/modules/ModalModule.groovy import geb.Module class ModalModule extends Module { static content = { title { $('[data-testid="modal-title"]') } body { $('[data-testid="modal-body"]') } confirmButton { $('[data-testid="modal-confirm"]') } cancelButton { $('[data-testid="modal-cancel"]') } closeButton { $('[data-testid="modal-close"]') } } void confirm() { confirmButton.click() waitFor { !isDisplayed() } } void cancel() { cancelButton.click() waitFor { !isDisplayed() } } boolean isDisplayed() { try { title.displayed } catch (Exception e) { false } } } // src/test/groovy/modules/TableModule.groovy import geb.Module class TableModule extends Module { static content = { headers { $('thead th') } rows { $('tbody tr') } cells { $('tbody td') } } List<String> getHeaderTexts() { headers*.text() } int getRowCount() { rows.size() } String getCellText(int row, int col) { rows[row].find('td')[col].text() } void clickRow(int index) { rows[index].click() } void sortBy(String headerText) { headers.find { it.text() == headerText }.click() } }
Spock Test Specifications
// src/test/groovy/specs/auth/LoginSpec.groovy import geb.spock.GebReportingSpec import pages.LoginPage import pages.DashboardPage import spock.lang.Unroll class LoginSpec extends GebReportingSpec { def "should login successfully with valid credentials"() { given: "I am on the login page" to LoginPage when: "I enter valid credentials and submit" login('user@example.com', 'SecurePass123') then: "I should be on the dashboard" at DashboardPage welcomeMessage.text().contains('Welcome') } def "should show error for invalid credentials"() { given: "I am on the login page" to LoginPage when: "I enter invalid credentials" login('user@example.com', 'wrongpassword') then: "I should see an error message" at LoginPage waitFor { errorMessage.displayed } errorMessage.text() == 'Invalid credentials' } @Unroll def "should show validation error for email=#email, password=#password"() { given: "I am on the login page" to LoginPage when: "I enter invalid data" login(email, password) then: "I should see the error" waitFor { errorMessage.displayed } errorMessage.text() == expectedError where: email | password | expectedError '' | 'SecurePass123' | 'Email is required' 'user@example.com' | '' | 'Password is required' 'invalid-email' | 'SecurePass123' | 'Invalid email format' } def "should navigate to forgot password page"() { given: "I am on the login page" to LoginPage when: "I click forgot password" forgotPasswordLink.click() then: "I should be on the forgot password page" waitFor { browser.currentUrl.contains('/forgot-password') } } }
Advanced Navigator Usage
// src/test/groovy/specs/shopping/CartSpec.groovy import geb.spock.GebReportingSpec import pages.CartPage import pages.ProductPage class CartSpec extends GebReportingSpec { def "should add product to cart"() { given: "I am on a product page" go '/products/laptop-pro' at ProductPage when: "I click add to cart" addToCartButton.click() then: "cart count should increase" waitFor { navBar.cartCount.text() == '1' } } def "should demonstrate navigator features"() { when: "using various navigator methods" go '/products' then: "jQuery-like selection works" // CSS selector $('div.product-card').size() > 0 // Attribute selector $('input', name: 'search').displayed // Text content $('h1', text: 'Products').displayed // Index-based $('div.product-card', 0).displayed // Chaining $('div.product-card').find('button.add-to-cart').size() > 0 // Filtering $('div.product-card').filter('.featured').size() >= 0 // Traversing $('[data-testid="product-1"]').parent().hasClass('product-grid') // Multiple elements iteration $('div.product-card').collect { it.find('h3').text() }.size() > 0 } def "should handle dynamic content with waitFor"() { given: "I am on the products page" go '/products' when: "I search for a product" $('input[type="search"]').value('laptop') $('button[type="submit"]').click() then: "results load dynamically" waitFor(15) { $('[data-testid="search-results"]').displayed } waitFor { $('div.product-card').size() > 0 } and: "I can interact with results" def firstProduct = $('div.product-card', 0) firstProduct.find('h3').text().toLowerCase().contains('laptop') } }
Form Handling
// src/test/groovy/specs/forms/FormSpec.groovy import geb.spock.GebReportingSpec class FormSpec extends GebReportingSpec { def "should fill and submit a form"() { given: "I am on the registration form" go '/register' when: "I fill in all fields" $('[data-testid="name"]').value('John Doe') $('[data-testid="email"]').value('john@example.com') $('[data-testid="password"]').value('SecurePass123') // Select dropdown $('select[name="country"]').value('US') // Radio button $('input[name="gender"]', value: 'male').click() // Checkbox $('[data-testid="terms"]').value(true) // File upload $('input[type="file"]').value(new File('src/test/resources/avatar.jpg').absolutePath) // Textarea $('textarea[name="bio"]').value('A short biography') // Submit $('[data-testid="register-submit"]').click() then: "registration succeeds" waitFor { browser.currentUrl.contains('/welcome') } } }
Gradle Build Configuration
// build.gradle plugins { id 'groovy' id 'java' } ext { gebVersion = '7.0' seleniumVersion = '4.18.1' spockVersion = '2.3-groovy-4.0' } repositories { mavenCentral() } dependencies { testImplementation "org.gebish:geb-spock:${gebVersion}" testImplementation "org.gebish:geb-core:${gebVersion}" testImplementation "org.spockframework:spock-core:${spockVersion}" testImplementation "org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}" testImplementation "org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}" testImplementation "org.seleniumhq.selenium:selenium-support:${seleniumVersion}" testRuntimeOnly 'org.apache.groovy:groovy-all:4.0.18' } test { useJUnitPlatform() systemProperty 'geb.env', System.getProperty('geb.env', 'chrome') systemProperty 'geb.build.reportsDir', reporting.baseDir.toString() + '/geb' } tasks.register('smokeTest', Test) { useJUnitPlatform { includeTags 'smoke' } }
CI/CD Integration (GitHub Actions)
name: Geb E2E Tests on: [push, pull_request] jobs: geb-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' - name: Start application run: ./gradlew bootRun & - name: Wait for app run: | for i in $(seq 1 30); do curl -s http://localhost:3000/health && break sleep 2 done - name: Run Geb tests run: ./gradlew test -Dgeb.env=chrome env: BASE_URL: http://localhost:3000 CI: true - uses: actions/upload-artifact@v4 if: always() with: name: geb-reports path: build/reports/
Best Practices
- Use
blocks in Page Objects for all element references. The content DSL provides lazy evaluation and automatic waiting.static content - Define
checkers on every Page class. Geb uses these to verify successful page navigation automatically.at - Use Modules for reusable UI components. Navigation bars, modals, and tables should be modules embedded in Pages.
- Use
for dynamic content instead ofwaitFor {}
. Geb polls the condition at configurable intervals.Thread.sleep() - Use
for optional content elements that may not be present on every page load.required: false - Use
as the base class to automatically capture page state (screenshots + HTML source) on test failure.GebReportingSpec - Configure environments in
for different browsers and execution contexts (local, CI, headless).GebConfig.groovy - Use Spock's
with@Unroll
blocks for parameterized tests to test multiple data scenarios cleanly.where: - Use
andto
for page navigation.via
navigates and verifies;to LoginPage
just navigates.via LoginPage - Keep Geb configuration separate in
. Do not hardcode browser configuration in test classes.GebConfig.groovy
Anti-Patterns to Avoid
- Avoid raw
calls in tests — Encapsulate all selectors in Page content blocks. Raw selectors in specs break encapsulation.$() - Avoid
— Use Geb'sThread.sleep()
which polls intelligently. Fixed waits waste time or cause flakiness.waitFor {} - Avoid missing
checkers — Withoutat
blocks, Geb cannot verify page transitions, leading to confusing failures.at - Avoid monolithic Page Objects — Split large pages into Modules. A Page with 30+ content definitions needs decomposition.
- Avoid fragile CSS selectors — Use
attributes or meaningful selectors. Avoiddata-testid
..btn:nth-child(3) - Avoid test coupling — Each spec should be independent. Do not rely on test execution order or shared browser state.
- Avoid hardcoded base URLs — Use
and environment variables. Hardcoded URLs break portability.GebConfig.groovy - Avoid ignoring Geb's Navigator API — Geb's
provides powerful querying (filtering, traversal, text matching). Learn and use it fully.$() - Avoid missing
— Content that may not exist must userequired: false
or tests fail withrequired: false
.RequiredPageContentNotPresent - Avoid testing in a single browser only — Use Geb's environment configuration to run tests across Chrome, Firefox, and headless modes.