# 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

```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

```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).
