Geb Page Objects and Modules: Building Maintainable Browser Tests

Geb Page Objects and Modules: Building Maintainable Browser Tests

Raw $() selectors scattered throughout test files break the moment your UI changes. Geb's Page Object support is built into the framework — not a convention bolted on top — and it gives you a structured way to isolate selectors, model navigation flows, and reuse UI components.

The Page Class

A Geb Page represents one screen in your application. It defines:

  1. url — the path to navigate to this page
  2. at checker — condition that must be true when you're on this page
  3. content — named selectors for elements on the page
import geb.Page

class LoginPage extends Page {
    static url = '/login'

    static at = { title == 'Sign In — MyApp' }

    static content = {
        emailField { $('input[name=email]') }
        passwordField { $('input[name=password]') }
        submitButton { $('button[type=submit]') }
        errorMessage(required: false) { $('div.alert-error') }
    }

    void loginAs(String email, String password) {
        emailField.value(email)
        passwordField.value(password)
        submitButton.click()
    }
}

Using the Page in Tests

import geb.spock.GebSpec

class LoginSpec extends GebSpec {

    def "valid credentials redirect to dashboard"() {
        given:
        to LoginPage

        when:
        loginAs('alice@example.com', 'password123')

        then:
        at DashboardPage
    }

    def "invalid credentials show error"() {
        given:
        to LoginPage

        when:
        loginAs('alice@example.com', 'wrongpassword')

        then:
        waitFor { errorMessage.displayed }
        errorMessage.text().contains('Invalid credentials')
    }
}

to LoginPage navigates to the page's URL and verifies the at checker. at DashboardPage asserts you've arrived at the expected page.

Content DSL Options

The content {} block supports several options that control how elements behave:

static content = {
    // required: false — don't fail if element is absent
    flashMessage(required: false) { $('div.flash') }

    // wait: true — wait for element to appear before returning
    resultsTable(wait: true) { $('table.results') }

    // cache: true — don't re-evaluate the selector on each access
    pageHeader(cache: true) { $('h1') }

    // wait with timeout
    loadingSpinner(wait: 10, required: false) { $('div.spinner') }

    // parameterized content
    rowByIndex { index -> $('table tr', index + 1) }  // skip header row
    cellByText { text -> $('td', text: text) }
}

Parameterized content lets you pass arguments to selectors:

// In test:
page.rowByIndex(2).click()
page.cellByText('Pending').text()

The Module Class

Modules model reusable UI components — navigation bars, form sections, data table rows, modals — that appear on multiple pages.

import geb.Module

class NavbarModule extends Module {
    static content = {
        logo { $('a.navbar-brand') }
        searchInput { $('input[name=search]') }
        userMenu { $('div.user-menu') }
        logoutLink { userMenu.$('a[href=/logout]') }
    }

    void searchFor(String term) {
        searchInput.value(term)
        searchInput << Keys.ENTER
    }
}

Use a module in a page by including it in the content block:

class DashboardPage extends Page {
    static url = '/dashboard'
    static at = { currentUrl.contains('/dashboard') }

    static content = {
        navbar { module NavbarModule }
        projectList { $('ul.projects') }
        newProjectButton { $('a.new-project') }
    }
}
// In test:
def "user can search from dashboard"() {
    given:
    to DashboardPage

    when:
    navbar.searchFor('testing tools')

    then:
    at SearchResultsPage
}

Nested Modules

Modules can contain other modules. This is useful for table rows that have repeating sub-components:

class TableRowModule extends Module {
    static content = {
        nameCell { $('td', 0) }
        statusCell { $('td', 1) }
        actionMenu { module ActionMenuModule, $('td', 2) }
    }
}

class DataTablePage extends Page {
    static content = {
        rows { moduleList TableRowModule, $('table tbody tr') }
    }
}

moduleList creates a list of modules, one per matched element:

def "table shows all active projects"() {
    given:
    to DataTablePage

    expect:
    rows.size() == 5
    rows[0].nameCell.text() == 'Project Alpha'
    rows[0].statusCell.text() == 'Active'
}

Geb provides multiple navigation methods:

// Navigate and verify at checker passes
to LoginPage

// Navigate without at check
via LoginPage

// Assert current page (verifies at checker)
at DashboardPage

// Navigate and return page instance
def page = browser.to(LoginPage)

Pages can define where they expect to navigate after an action:

class LoginPage extends Page {
    static content = {
        submitButton { $('button[type=submit]') }
    }

    DashboardPage loginAs(String email, String password) {
        emailField.value(email)
        passwordField.value(password)
        submitButton.click()
        browser.at(DashboardPage)
    }
}

Organizing Page Objects

A clean structure for mid-size projects:

src/test/groovy/
  pages/
    LoginPage.groovy
    DashboardPage.groovy
    SettingsPage.groovy
  modules/
    NavbarModule.groovy
    ModalModule.groovy
    DataTableModule.groovy
  specs/
    LoginSpec.groovy
    DashboardSpec.groovy

Keep pages in pages/, modules in modules/, and tests in specs/. Page objects should never contain assertions — assertions live in specs.

Common Mistakes

Assertions in page objects: Page objects should model the UI, not assert on it. Assertions belong in the spec's then: block. This makes page objects reusable across specs with different expected outcomes.

Over-caching: cache: true means Geb won't re-query the DOM. Use it for static elements (headers, labels). Don't cache dynamic content — you'll read stale values.

Missing required: false: Elements that are conditionally present (error messages, loading indicators, empty states) need required: false. Otherwise Geb throws if they're absent when the test accesses them.

Summary

Geb's Page Objects and Modules provide a structured way to manage the growing complexity of browser test suites. Pages encapsulate navigation and selectors for a single screen. Modules encapsulate reusable components. Together they keep selectors in one place — meaning a UI change requires updating one file, not hunting through thirty test files.

The content DSL options (wait, required, cache) give you control over how elements are located, making tests more reliable without adding boilerplate waiting logic to every test.

Read more

Start now free