Files
freemo d9e5668cec fix(skills): comprehensive final audit pass for programming-patterns skill
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
2026-04-15 13:22:13 -04:00

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.");
  • 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).