Page Modules

Page modules provide a structured, reusable way to organize your web automation tests. Instead of scattering element locators throughout your test code, page modules encapsulate page structure in dedicated modules that can be reused across multiple tests.

Without page modules, tests require inline locators and element definitions, making them verbose and harder to maintain:

let assert Ok(output) =
  driver.new(browser.Firefox)
  |> driver.goto("https://gleam.run/")
  |> get.node(by.xpath(
    "//div[@class='hero']//a[@href='https://tour.gleam.run/']",
  ))
  |> node.do(action.click(key.LeftClick))
  |> get.node(by.css("pre.log"))
  |> node.get(node.text())
  |> driver.close()

With page modules, the same test becomes more readable and the locators can be reused:

let assert Ok(output) =
  driver.new(browser.Firefox)
  |> driver.goto("https://gleam.run/")
  |> gleam_page.tour_button(action.click(key.LeftClick))
  |> gleam_page.log_output(node.text())
  |> driver.close()

Creating a Page Module

A page module is a regular Gleam module that defines functions for each element on a page. Each function takes a WebDriver and an action, then performs that action on the element.

Basic Structure

import butterbee/by
import butterbee/driver
import butterbee/page_module/element
import butterbee/webdriver.{type WebDriver}

// Navigate to the page
pub fn goto(driver: WebDriver(state)) {
  driver.goto(driver, "https://example.com/login")
}

// Define page elements
pub fn username_field(
  driver: WebDriver(state),
  action: fn(_) -> WebDriver(new_state),
) {
  element.define(field: by.css("input#username"))
  |> element.perform_action(driver, action)
}

pub fn password_field(
  driver: WebDriver(state),
  action: fn(_) -> WebDriver(new_state),
) {
  element.define(field: by.css("input#password"))
  |> element.perform_action(driver, action)
}

pub fn login_button(
  driver: WebDriver(state),
  action: fn(_) -> WebDriver(new_state),
) {
  element.define(field: by.css("button[type='submit']"))
  |> element.perform_action(driver, action)
}

Then use the page module in your test:

import login_page

pub fn login_test() {
  let assert Ok(_) =
    driver.new(Firefox)
    |> login_page.goto()
    |> login_page.username_field(node.set_value("testuser"))
    |> login_page.password_field(node.set_value("password123"))
    |> login_page.login_button(action.click(key.LeftClick))
    |> driver.close()
}

Element Types

Page modules support different element types for common HTML structures:

Basic Elements

Use element for standard HTML elements like inputs, buttons, links, and divs:

import butterbee/page_module/element

pub fn submit_button(
  driver: WebDriver(state),
  action: fn(_) -> WebDriver(new_state),
) {
  element.define(field: by.css("button#submit"))
  |> element.perform_action(driver, action)
}

Select Elements (Dropdowns)

Example Select element:

Use select_element for <select> dropdowns with specialized actions:

import butterbee/page_module/select_element

pub fn pokemon_dropdown(
  driver: WebDriver(state),
  action: fn(_) -> WebDriver(new_state),
) {
  select_element.define(field: by.css("select#pokemon"))
  |> select_element.perform_action(driver, action)
}

Then perform actions on the dropdown in your test:

// Select an option by its visible text
driver
|> form_page.pokemon_dropdown(select_element.option("Charmander"))

// Get the currently selected option's text
driver
|> form_page.pokemon_dropdown(select_element.selected_text())

Table Elements

Example Table element:

ID Name Type
25 Pikachu Electric
4 Charmander Fire
573 Cinccino Normal

Use table_element to work with HTML tables, accessing the entire table, specific rows, or individual cells:

import butterbee/page_module/table_element.{type NodeTable}

pub fn pokedex_table(
  driver: WebDriver(state),
  on_element: NodeTable,
  action: fn(_) -> WebDriver(new_state),
) {
  table_element.define(
    table: by.css("table#pokedex"),
    table_row: by.css("tr"),
    table_cell: by.css("td"),
    table_width: 3,
  )
  |> table_element.perform_action(driver, on_element, action)
}

Then perform actions on the table in your test

// Get entire table text
let assert Ok(table_text) =
  driver
  |> pokedex_page.pokedex_table(table_element.Table, node.inner_text())
  |> driver.value()

// Get text from row 1 (0-indexed, so this is the second row)
let assert Ok(row_text) =
  driver
  |> pokedex_page.pokedex_table(table_element.Row(1), node.inner_text())
  |> driver.value()
// Result: "25\tPikachu\tElectric"

// Get text from cell at row 1, column 1
let assert Ok(cell_text) =
  driver
  |> pokedex_page.pokedex_table(table_element.Cell(1, 1), node.inner_text())
  |> driver.value()
// Result: "Pikachu"

Note: When defining a table element, you must specify the table_width (number of columns) to correctly calculate cell positions.

List Elements

Example List element:

Use list_element for ordered or unordered lists:

import butterbee/page_module/list_element.{type NodeList}

pub fn team_list(
  driver: WebDriver(state),
  on_element: NodeList,
  action: fn(_) -> WebDriver(new_state),
) {
  list_element.define(
    list: by.css("ul#team"),
    list_item: by.css("li"),
  )
  |> list_element.perform_action(driver, on_element, action)
}

Then perform actions on the list in your test:

// Get entire list text
let assert Ok(list_text) =
  driver
  |> team_page.team_list(list_element.List, node.inner_text())
  |> driver.value()
// Result: "Pikachu\nCharmander\nBulbasaur\nSquirtle\nJigglypuff"

// Get text from the second item (0-indexed)
let assert Ok(item_text) =
  driver
  |> team_page.team_list(list_element.Row(1), node.inner_text())
  |> driver.value()
// Result: "Charmander"

// Click the third item
driver
|> team_page.team_list(list_element.Row(2), action.click(key.LeftClick))
Search Document