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:

use driver <- butterbee.run([Firefox])
let assert Ok(output) =
  driver
  |> butterbee.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())
  |> butterbee.close()

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

use driver <- butterbee.run([Firefox])
let assert Ok(output) =
  driver
  |> gleam_page.goto()
  |> gleam_page.tour_button(action.click(key.LeftClick))
  |> gleam_page.log_output(node.text())
  |> butterbee.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

// Navigate to the page
pub fn goto(driver: WebDriver(state)) {
  butterbee.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(driver, action)
}

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

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

Then use the page module in your test:

import login_page

pub fn login_test() {
  let assert Ok(_) =
    driver
    |> 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))
    |> butterbee.close()
}

Element Types

Page modules support different element types for common HTML structures:

Basic Elements

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

import butterbee/element

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

Select Elements (Dropdowns)

Example Select element:

Use normal element.define definitions for <select> dropdowns:

import butterbee/element

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

Then perform actions on the dropdown in your test. node.select_option and node.selected_text are specialized actions for <select> elements:

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

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

Table Elements

Example Table element:

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

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

import butterbee/element

pub fn pokedex_table(
  driver: WebDriver(state),
  on_element: element.NodeTable,
  action: fn(_) -> WebDriver(new_state),
) {
  element.define_table(
    table: by.css("table#pokedex"),
    table_row: by.css("tr"),
    table_cell: by.css("td"),
    table_width: 3,
  )
  |> element.perform_on_table(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(element.Table, node.inner_text())
  |> butterbee.value()

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

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

List Elements

Example List element:

Use element.define_list for ordered and unordered lists:

import butterbee/element

pub fn team_list(
  driver: WebDriver(state),
  on_element: element.NodeList,
  action: fn(_) -> WebDriver(new_state),
) {
  element.define_list(
    list: by.css("ul#team"),
    list_item: by.css("li"),
  )
  |> element.perform_on_list(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(element.List, node.inner_text())
  |> butterbee.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(element.Row(1), node.inner_text())
  |> butterbee.value()
// Result: "Charmander"

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