d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
9.1 KiB
9.1 KiB
Page Object
Problem
UI tests are tightly coupled to page structure — selectors, element IDs, and DOM hierarchy are scattered throughout test code. When the UI changes, dozens of tests break even though the behaviour hasn't changed. Tests are verbose, duplicative, and fragile.
Solution
Encapsulate each page (or significant component) behind a Page Object class that:
- Exposes actions as methods (e.g.,
login(user, password)). - Exposes state queries as properties (e.g.,
error_message,is_logged_in). - Hides all selectors, waits, and DOM interaction details internally.
- Returns other Page Objects when navigation occurs.
Tests interact only with Page Object APIs, never with raw selectors.
When to Use
- Selenium, Playwright, Cypress, or any browser-automation test suite.
- Mobile UI testing (Appium).
- Any UI test that touches more than one or two elements on a page.
When to Avoid
- Simple API or unit tests with no UI.
- One-off scripts that won't be maintained.
- When the framework already provides a similar abstraction (e.g., Testing Library's approach discourages page objects in favour of user-centric queries).
Pseudocode
class LoginPage:
url = "/login"
function navigate():
browser.goto(url)
function login(username, password):
find("#username").type(username)
find("#password").type(password)
find("#submit").click()
return DashboardPage()
function get error_message():
return find(".error").text
class DashboardPage:
function get welcome_text():
return find("#welcome").text
// Test
test "successful login shows welcome":
page = LoginPage()
page.navigate()
dashboard = page.login("alice", "s3cret")
assert dashboard.welcome_text == "Welcome, alice!"
Python
"""Page Object pattern — simulated browser for runnable demonstration."""
from dataclasses import dataclass, field
# ── Simulated browser / DOM ───────────────────────────────────
class FakeBrowser:
"""Minimal browser simulator for demonstration purposes."""
def __init__(self):
self.current_url: str = ""
self.elements: dict[str, str] = {}
self.inputs: dict[str, str] = {}
self._users = {"alice": "s3cret", "bob": "p4ssw0rd"}
def goto(self, url: str) -> None:
self.current_url = url
if url == "/login":
self.elements = {
"#username": "", "#password": "", "#submit": "Login",
".error": "",
}
def type_into(self, selector: str, value: str) -> None:
self.inputs[selector] = value
def click(self, selector: str) -> None:
if selector == "#submit" and self.current_url == "/login":
user = self.inputs.get("#username", "")
pwd = self.inputs.get("#password", "")
if self._users.get(user) == pwd:
self.current_url = "/dashboard"
self.elements = {"#welcome": f"Welcome, {user}!"}
else:
self.elements[".error"] = "Invalid credentials"
def text(self, selector: str) -> str:
return self.elements.get(selector, "")
# ── Page Objects ──────────────────────────────────────────────
class LoginPage:
URL = "/login"
def __init__(self, browser: FakeBrowser):
self.browser = browser
def navigate(self) -> "LoginPage":
self.browser.goto(self.URL)
return self
def login(self, username: str, password: str) -> "DashboardPage":
self.browser.type_into("#username", username)
self.browser.type_into("#password", password)
self.browser.click("#submit")
return DashboardPage(self.browser)
@property
def error_message(self) -> str:
return self.browser.text(".error")
class DashboardPage:
def __init__(self, browser: FakeBrowser):
self.browser = browser
@property
def welcome_text(self) -> str:
return self.browser.text("#welcome")
@property
def is_on_dashboard(self) -> bool:
return self.browser.current_url == "/dashboard"
# ── Tests ─────────────────────────────────────────────────────
def test_successful_login():
browser = FakeBrowser()
login_page = LoginPage(browser).navigate()
dashboard = login_page.login("alice", "s3cret")
assert dashboard.is_on_dashboard
assert dashboard.welcome_text == "Welcome, alice!"
print(f"PASS: {dashboard.welcome_text}")
def test_failed_login_shows_error():
browser = FakeBrowser()
login_page = LoginPage(browser).navigate()
login_page.login("bob", "wrong")
assert login_page.error_message == "Invalid credentials"
print(f"PASS: error shown = \"{login_page.error_message}\"")
def test_page_object_hides_selectors():
"""Tests never reference selectors directly."""
browser = FakeBrowser()
login_page = LoginPage(browser).navigate()
dashboard = login_page.login("alice", "s3cret")
# Only high-level API is used — no "#username", ".error" etc.
assert dashboard.welcome_text.startswith("Welcome")
print(f"PASS: selectors hidden, API used: welcome_text={dashboard.welcome_text}")
if __name__ == "__main__":
test_successful_login()
test_failed_login_shows_error()
test_page_object_hides_selectors()
print("\nAll Page Object tests passed.")
JavaScript
// page_object.js — Page Object pattern with simulated browser
// ── Simulated browser / DOM ──────────────────────────────────
class FakeBrowser {
constructor() {
this.currentUrl = "";
this.elements = {};
this.inputs = {};
this._users = { alice: "s3cret" };
}
goto(url) {
this.currentUrl = url;
if (url === "/login") {
this.elements = {
"#username": "",
"#password": "",
"#submit": "Login",
".error": "",
};
}
}
typeInto(selector, value) {
this.inputs[selector] = value;
}
click(selector) {
if (selector === "#submit" && this.currentUrl === "/login") {
const user = this.inputs["#username"] || "";
const pwd = this.inputs["#password"] || "";
if (this._users[user] === pwd) {
this.currentUrl = "/dashboard";
this.elements = { "#welcome": `Welcome, ${user}!` };
} else {
this.elements[".error"] = "Invalid credentials";
}
}
}
text(selector) {
return this.elements[selector] || "";
}
}
// ── Page Objects ─────────────────────────────────────────────
class LoginPage {
static URL = "/login";
constructor(browser) {
this.browser = browser;
}
navigate() {
this.browser.goto(LoginPage.URL);
return this;
}
login(username, password) {
this.browser.typeInto("#username", username);
this.browser.typeInto("#password", password);
this.browser.click("#submit");
return new DashboardPage(this.browser);
}
get errorMessage() {
return this.browser.text(".error");
}
}
class DashboardPage {
constructor(browser) {
this.browser = browser;
}
get welcomeText() {
return this.browser.text("#welcome");
}
get isOnDashboard() {
return this.browser.currentUrl === "/dashboard";
}
}
// ── Tests ────────────────────────────────────────────────────
function testSuccessfulLogin() {
const browser = new FakeBrowser();
const loginPage = new LoginPage(browser).navigate();
const dashboard = loginPage.login("alice", "s3cret");
console.assert(dashboard.isOnDashboard);
console.assert(dashboard.welcomeText === "Welcome, alice!");
console.log(`PASS: ${dashboard.welcomeText}`);
}
function testFailedLoginShowsError() {
const browser = new FakeBrowser();
const loginPage = new LoginPage(browser).navigate();
loginPage.login("bob", "wrong");
console.assert(loginPage.errorMessage === "Invalid credentials");
console.log(`PASS: error shown = "${loginPage.errorMessage}"`);
}
function testPageObjectHidesSelectors() {
const browser = new FakeBrowser();
const loginPage = new LoginPage(browser).navigate();
const dashboard = loginPage.login("alice", "s3cret");
console.assert(dashboard.welcomeText.startsWith("Welcome"));
console.log(
`PASS: selectors hidden, API used: welcomeText=${dashboard.welcomeText}`
);
}
testSuccessfulLogin();
testFailedLoginShowsError();
testPageObjectHidesSelectors();
console.log("\nAll Page Object tests passed.");
Related Patterns
- Facade — the page object is a facade over the browser/DOM, exposing high-level actions and hiding selectors.
- Builder — page objects can expose fluent builder-style methods to chain UI actions readably.
- Composite — page objects for complex UIs can be composed into hierarchies (e.g., PageObject containing ComponentObject).