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:
- Pikachu
- Charmander
- Bulbasaur
- Squirtle
- Jigglypuff
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))