---
title: Your First Page Object in Selenium | TestingBot
description: In this chapter you write the `LoginPage` and `LoggedInPage` page objects
  for the practice site, in Java with TestNG and in Python with pytest. The two…
source_url:
  html: https://testingbot.com/resources/courses/page-object-model/first-page-object
  md: https://testingbot.com/resources/courses/page-object-model/first-page-object.md
---

Course

[Page Object Model and Framework Design](https://testingbot.com/resources/courses/page-object-model)

Chapter 2 of 10

1. [1 What Is the Page Object Model?](https://testingbot.com/resources/courses/page-object-model)
2. [2 Your First Page Object in Selenium](https://testingbot.com/resources/courses/page-object-model/first-page-object)
3. [3 Page Object Model in Playwright](https://testingbot.com/resources/courses/page-object-model/playwright-page-objects)
4. [4 Components and Fluent Page Objects](https://testingbot.com/resources/courses/page-object-model/components-and-fluent-pages)
5. [5 Page Factory vs Plain By Locators](https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators)
6. [6 Test Data, Config and Environments](https://testingbot.com/resources/courses/page-object-model/test-data-and-config)
7. [7 Test Automation Framework Structure](https://testingbot.com/resources/courses/page-object-model/framework-layout)
8. [8 Run the Framework in Parallel on a Grid](https://testingbot.com/resources/courses/page-object-model/parallel-on-a-grid)
9. [9 POM Anti-Patterns and When to Skip POM](https://testingbot.com/resources/courses/page-object-model/anti-patterns)
10. [10 Knowledge check](https://testingbot.com/resources/courses/page-object-model/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 2 of 10 Intermediate 8 min read 3 quick checks Updated Nov 2026 

# Your First Page Object in Selenium
On this page
1. [Look at the page first](https://testingbot.com#look-at-the-page-first)
2. [Project setup](https://testingbot.com#project-setup)
3. [A small base class](https://testingbot.com#a-small-base-class)
4. [The login page](https://testingbot.com#the-login-page)
5. [The landing page](https://testingbot.com#the-landing-page)
6. [The tests](https://testingbot.com#the-tests)

In this chapter you write the `LoginPage` and `LoggedInPage` page objects for the practice site, in Java with TestNG and in Python with pytest. The two versions are deliberately parallel so you can read whichever language you work in and still follow the rest of the course.

## Look at the page first[#](https://testingbot.com#look-at-the-page-first)

Before writing a page object, open the page and list two things: what a user can **do** here, and what a user can **see** here. For `https://practicetestautomation.com/practice-test-login/`:

| Do | See |
| --- | --- |
| Type a username (`#username`) | An error box (`#error`) with "Your username is invalid!" or "Your password is invalid!" |
| Type a password (`#password`) | |
| Press Submit (`#submit`) | |

A successful login lands on `/logged-in-successfully/`, which has an `h1` "Logged In Successfully", a bold "Congratulations student. You successfully logged in!" and a "Log out" link.

Those two columns become the public API of your page object: actions become methods, things to see become getters. Locators stay private.

One detail is worth checking in DevTools now, because it will bite later: `#error` is in the DOM from the moment the page loads, with the text "Your username is invalid!" already in it. It is hidden with `opacity: 0` and only gets a `show` class after a failed attempt. Selenium's visibility check treats an element with opacity 0 as not displayed, so waiting for visibility works here. (Playwright does not, as chapter 02 shows.)

From the Do / See table, what should the login page object expose publicly?

 Public `By` fields for username, password and submit, so tests can build their own steps Methods for what the user does (log in) and getters for what the user sees (the error text), with locators kept private A `getErrorElement()` method that returns the `WebElement`

Actions become methods and visible things become getters. Public locators or elements let tests start clicking around inside the page object's territory again.

## Project setup[#](https://testingbot.com#project-setup)

Java, with Maven:

```xml
<dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.52.0</version>
  </dependency>
  <dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>7.12.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>
```

Python:

```bash
python -m venv .venv && source .venv/bin/activate
pip install selenium==4.52.0 pytest==9.2.0
```

Selenium 4 ships Selenium Manager, which downloads a matching chromedriver for you, so neither setup needs a driver binary on the PATH.

## A small base class[#](https://testingbot.com#a-small-base-class)

Every page object needs the driver and a wait. Rather than repeating that, put it in a base class together with the two or three interactions every page uses. Keep this class small; chapter 08 explains what happens when base pages grow.

```java
package com.example.pages;

import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public abstract class BasePage {
    protected final WebDriver driver;
    protected final WebDriverWait wait;

    protected BasePage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    protected WebElement visible(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    protected void type(By locator, String text) {
        WebElement field = visible(locator);
        field.clear();
        field.sendKeys(text);
    }

    protected void click(By locator) {
        wait.until(ExpectedConditions.elementToBeClickable(locator)).click();
    }
}
```

```python
# pages/base_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

class BasePage:
    def __init__ (self, driver: WebDriver, timeout: float = 10):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)

    def visible(self, locator):
        return self.wait.until(EC.visibility_of_element_located(locator))

    def type(self, locator, text: str) -> None:
        field = self.visible(locator)
        field.clear()
        field.send_keys(text)

    def click(self, locator) -> None:
        self.wait.until(EC.element_to_be_clickable(locator)).click()
```

Every interaction waits explicitly. That is the main reason page objects reduce flakiness: the waiting strategy is written once, correctly, instead of 300 times with varying care.

Why does routing every interaction through `BasePage` helpers such as `visible()` and `click()` reduce flakiness?

 The helpers retry the whole test when an action fails They set a global implicit wait on the driver Each helper waits explicitly for the right condition, so the waiting strategy is written once instead of in every test

The wait is part of the helper, so every page object interaction waits the same correct way. Nothing is retried and no implicit wait is involved.

## The login page[#](https://testingbot.com#the-login-page)

```java
package com.example.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class LoginPage extends BasePage {
    private static final String URL = "https://practicetestautomation.com/practice-test-login/";

    private final By username = By.id("username");
    private final By password = By.id("password");
    private final By submit = By.id("submit");
    private final By error = By.id("error");

    public LoginPage(WebDriver driver) {
        super(driver);
    }

    public LoginPage open() {
        driver.get(URL);
        visible(username);
        return this;
    }

    public LoggedInPage loginAs(String user, String pass) {
        submitCredentials(user, pass);
        return new LoggedInPage(driver);
    }

    public LoginPage loginExpectingError(String user, String pass) {
        submitCredentials(user, pass);
        return this;
    }

    public String errorMessage() {
        return visible(error).getText();
    }

    private void submitCredentials(String user, String pass) {
        type(username, user);
        type(password, pass);
        click(submit);
    }
}
```

```python
# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC

from pages.base_page import BasePage

class LoginPage(BasePage):
    USERNAME = (By.ID, "username")
    PASSWORD = (By.ID, "password")
    SUBMIT = (By.ID, "submit")
    ERROR = (By.ID, "error")

    def __init__ (self, driver, base_url: str):
        super(). __init__ (driver)
        self.base_url = base_url

    def open(self) -> "LoginPage":
        self.driver.get(f"{self.base_url}/practice-test-login/")
        self.visible(self.USERNAME)
        return self

    def login_as(self, user: str, password: str) -> "LoggedInPage":
        self._submit(user, password)
        return LoggedInPage(self.driver)

    def login_expecting_error(self, user: str, password: str) -> "LoginPage":
        self._submit(user, password)
        return self

    @property
    def error_message(self) -> str:
        return self.visible(self.ERROR).text

    def _submit(self, user: str, password: str) -> None:
        self.type(self.USERNAME, user)
        self.type(self.PASSWORD, password)
        self.click(self.SUBMIT)
```

Three design decisions to notice:

- **Two login methods, not one.** `loginAs` returns the page you land on after success; `loginExpectingError` returns the login page itself. The return type documents the expected outcome, and a test that calls the wrong one fails at the next step with a clear message instead of an assertion on the wrong page.
- **The getter returns data, not a `WebElement`.** Tests get a `String`, so they cannot accidentally start clicking around inside the page object's territory.
- **`open()` waits for the page to be usable** before returning. A page object that returns while the page is still loading moves the race condition into the test.

## The landing page[#](https://testingbot.com#the-landing-page)

```java
package com.example.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;

public class LoggedInPage extends BasePage {
    private final By heading = By.tagName("h1");
    private final By message = By.cssSelector(".post-content strong");

    public LoggedInPage(WebDriver driver) {
        super(driver);
        // the one check a page object may make: are we on the right page?
        wait.until(ExpectedConditions.urlContains("/logged-in-successfully/"));
    }

    public String heading() { return visible(heading).getText(); }

    public String message() { return visible(message).getText(); }
}
```

In Python, put `LoggedInPage` in the same module as `LoginPage` (the two reference each other, and one module avoids a circular import):

```python
# pages/login_page.py, continued
class LoggedInPage(BasePage):
    HEADING = (By.TAG_NAME, "h1")
    MESSAGE = (By.CSS_SELECTOR, ".post-content strong")

    def __init__ (self, driver):
        super(). __init__ (driver)
        self.wait.until(EC.url_contains("/logged-in-successfully/"))

    @property
    def heading(self) -> str:
        return self.visible(self.HEADING).text

    @property
    def message(self) -> str:
        return self.visible(self.MESSAGE).text
```

The constructor check follows the Selenium documentation's single exception to "page objects should never make assertions": it is fine to verify that the page "and possibly critical elements on the page, were loaded correctly" while instantiating the page object.

## The tests[#](https://testingbot.com#the-tests)

```java
package com.example.tests;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.example.pages.LoggedInPage;
import com.example.pages.LoginPage;

public class LoginTest {
    private WebDriver driver;

    @BeforeMethod
    public void start() { driver = new ChromeDriver(); }

    @AfterMethod(alwaysRun = true)
    public void stop() { if (driver != null) driver.quit(); }

    @Test
    public void validUserSeesWelcomeMessage() {
        LoggedInPage home = new LoginPage(driver).open().loginAs("student", "Password123");

        assertEquals(home.heading(), "Logged In Successfully");
        assertTrue(home.message().contains("successfully logged in"));
    }

    @Test
    public void wrongPasswordShowsError() {
        LoginPage login = new LoginPage(driver).open()
                .loginExpectingError("student", "wrong-password");

        assertEquals(login.errorMessage(), "Your password is invalid!");
    }
}
```

```python
# conftest.py
import pytest
from selenium import webdriver

@pytest.fixture
def base_url() -> str:
    return "https://practicetestautomation.com"

@pytest.fixture
def driver():
    drv = webdriver.Chrome()
    yield drv
    drv.quit() # runs even when the test fails
```

```python
# tests/test_login.py
from pages.login_page import LoginPage

def test_valid_user_sees_welcome_message(driver, base_url):
    home = LoginPage(driver, base_url).open().login_as("student", "Password123")

    assert home.heading == "Logged In Successfully"
    assert "successfully logged in" in home.message

def test_wrong_password_shows_error(driver, base_url):
    login = LoginPage(driver, base_url).open().login_expecting_error(
        "student", "wrong-password")

    assert login.error_message == "Your password is invalid!"
```

Run them with `mvn test`, or with `pytest` from the project root (keep `conftest.py` there and add an empty `pages/ __init__.py` so `from pages.login_page import ...` resolves). The browser lifecycle now lives in `@BeforeMethod`/`@AfterMethod(alwaysRun = true)` and a yield fixture, which fixes the leaked-browser bug from the introduction.

The Java test keeps the driver in an instance field. That is fine while tests run one at a time; chapter 07 shows exactly how it breaks under `parallel="methods"` and replaces it with a `ThreadLocal`. If you want a fuller TestNG walkthrough, our [Selenium with Java and TestNG course](https://testingbot.com/resources/courses/selenium-testng) covers the runner itself, and the [pytest guide in our docs](https://testingbot.com/support/web-automate/selenium/python/pytest) covers the Python side.

An assertion in the middle of a test fails. Which of these still quits the browser?

 `driver.quit()` as the last line of the test method `@AfterMethod(alwaysRun = true)` in TestNG, or the code after `yield` in a pytest fixture A `quit()` call inside `LoggedInPage`'s constructor Nothing: a failed test always leaks its browser

A failed assertion throws, so lines after it never run. Teardown that the runner owns, `@AfterMethod(alwaysRun = true)` or fixture teardown after `yield`, runs regardless.

[Previous · Chapter 1 What Is the Page Object Model?](https://testingbot.com/resources/courses/page-object-model) [Next · Chapter 3 Page Object Model in Playwright](https://testingbot.com/resources/courses/page-object-model/playwright-page-objects)

On this page

1. [Look at the page first](https://testingbot.com#look-at-the-page-first)
2. [Project setup](https://testingbot.com#project-setup)
3. [A small base class](https://testingbot.com#a-small-base-class)
4. [The login page](https://testingbot.com#the-login-page)
5. [The landing page](https://testingbot.com#the-landing-page)
6. [The tests](https://testingbot.com#the-tests)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/page-object-model/certification)
