Page Object Model (POM)

Introduction

The Page Object Model (POM) is a pattern where you put selectors and page actions in separate classes (page objects) and use them in tests. This keeps tests readable and makes it easier to update selectors in one place when the UI changes.

CSTesting supports POM by letting you pass the browser into page object classes so they call browser.goto(), browser.click(), browser.locator(), etc. You create a pages/ folder and page classes yourself — the install wizard only scaffolds a sample test file.

You will learn

  • What the Page Object Model is and why to use it
  • How to set up a pages/ folder and write page object classes
  • How to write a page object and use it in tests
  • How to add more page objects (e.g. login, dashboard)

Why use Page Objects?

  • Single place for selectors — If a button's selector changes, you update it in the page object, not in every test.
  • Readable tests — Tests call loginPage.typeUsername('admin') instead of browser.type('[name="userName"]', 'admin').
  • Reuse — The same page object can be used in many tests and suites.
  • Clear structurepages/ holds page classes; tests/ holds test files.

Set up the structure

Create a pages/ folder for page object classes alongside your existing tests/ folder (created by npm init cstesting@latest or npx cstesting init):

mkdir pages
# add pages/HomePage.js (or HomePage.ts) and tests/home.test.js

A typical layout:

  • pages/ — folder for page object classes
  • tests/ — folder for test files

The examples below use pages/HomePage.js and tests/home.test.js — add these files yourself. Run the sample tests:

npx cstesting tests/
# or
npx cstesting tests/home.test.js

Structure

your-project/
  pages/
    HomePage.js      # Page object for the home page
    LoginPage.js     # Page object for the login page
    ...
  tests/
    home.test.js     # Tests using HomePage
    login.test.js    # Tests using LoginPage
    ...
  • Page objects — Classes that receive the browser and expose methods (e.g. goto(), clickSubmit(), getHeadingText()). They use this.browser to call goto, click, type, locator, waitForLoad, etc.
  • Tests — Create the browser (e.g. in beforeAll), instantiate page objects with that browser, then call page object methods and use expect() for assertions.

Example: HomePage

Here is a sample pages/HomePage.js you can create:

/**
 * Page Object: Home page (example.com).
 * Centralizes selectors and page actions — use in tests for maintainability.
 */

class HomePage {
  constructor(browser) {
    this.browser = browser;
  }

  /** Page URL */
  get url() {
    return 'https://example.com';
  }

  /** Navigate to the home page */
  async goto() {
    await this.browser.goto(this.url);
    await this.browser.waitForLoad();
  }

  /** Get the main heading text */
  async getHeadingText() {
    const text = await this.browser.evaluate(
      "document.querySelector('h1') ? document.querySelector('h1').textContent : ''"
    );
    return text;
  }

  /** Click the "More information..." link */
  async clickMoreInfo() {
    const link = this.browser.locator('a');
    await link.click();
    await this.browser.waitForLoad();
  }

  /** Get page title */
  async getTitle() {
    return this.browser.evaluate('document.title');
  }
}

module.exports = HomePage;

tests/home.test.js uses it:

const path = require('path');
const cstesting = (() => {
  try { return require('cstesting'); } catch { return require(path.join(__dirname, '..')); }
})();
const { describe, it, expect, beforeAll, afterAll } = cstesting;
const HomePage = require('../pages/HomePage');

describe('Home page (POM)', () => {
  let browser;
  let homePage;

  beforeAll(async () => {
    browser = await cstesting.createBrowser({ headless: true });
    homePage = new HomePage(browser);
  });

  afterAll(async () => {
    if (browser) await browser.close();
  });

  it('should open home page and show Example Domain heading', async () => {
    await homePage.goto();
    const heading = await homePage.getHeadingText();
    expect(heading).toContain('Example Domain');
  });

  it('should have correct page title', async () => {
    await homePage.goto();
    const title = await homePage.getTitle();
    expect(title).toContain('Example');
  });

  it('should navigate when clicking More information link', async () => {
    await homePage.goto();
    await homePage.clickMoreInfo();
    const html = await browser.content();
    expect(html.length).toBeGreaterThan(0);
  });
});

Example: LoginPage (custom)

You can add more page objects for other screens. Example pages/LoginPage.js:

class LoginPage {
  constructor(browser) {
    this.browser = browser;
  }

  get url() {
    return 'https://example.com/login';
  }

  async goto() {
    await this.browser.goto(this.url);
    await this.browser.waitForLoad();
  }

  async typeUsername(value) {
    await this.browser.type('[name="userName"]', value);
  }

  async typePassword(value) {
    await this.browser.type('[name="password"]', value);
  }

  async submit() {
    await this.browser.click('[name="submit"]');
  }
}

module.exports = LoginPage;

tests/login.test.js:

const { describe, it, expect, beforeAll, afterAll, createBrowser } = require('cstesting');
const LoginPage = require('../pages/LoginPage');

describe('Login (POM)', () => {
  let browser;
  let loginPage;

  beforeAll(async () => {
    browser = await createBrowser({ headless: true });
    loginPage = new LoginPage(browser);
  });

  afterAll(async () => {
    if (browser) await browser.close();
  });

  it('logs in with valid credentials', async () => {
    await loginPage.goto();
    await loginPage.typeUsername('mercury');
    await loginPage.typePassword('secret');
    await loginPage.submit();
    const url = await browser.url();
    expect(url).not.toContain('/login');
  });
});

What you can do inside a page object

Page objects receive the browser (or a tab handle). You can use any browser API:

MethodUse in page object
this.browser.goto(url)Navigate
this.browser.click(selector)Click
this.browser.type(selector, text)Type into input
this.browser.locator(selector)Get locator: .click(), .type(text), .check(), .select(option)
this.browser.getByAttribute(attr, value)Locate by attribute
this.browser.select(selector, option)Select dropdown option
this.browser.check(selector) / uncheck(selector)Checkbox / radio
this.browser.waitForSelector(selector, { timeout })Wait for element
this.browser.waitForLoad()Wait for load event
this.browser.content()Get HTML string
this.browser.url()Get current URL
this.browser.evaluate(expression)Run JavaScript in the page

Keep selectors inside the page object; expose methods to the tests (e.g. clickSubmit(), getHeadingText()). Tests should not need to know CSS selectors.


Best practices

  1. One class per page (or major section) — e.g. HomePage, LoginPage, DashboardPage.
  2. Constructor takes browserconstructor(browser) { this.browser = browser; } so the same browser is reused across page objects if needed.
  3. Expose actions and getters, hide selectors — Tests call loginPage.typeUsername('x'), not browser.type('[name="userName"]', 'x').
  4. Optional: expose URL — A url getter or goto() method keeps navigation in one place.
  5. Use locators when it helps — e.g. this.browser.locator('button.primary').click() inside the page object to keep selectors out of tests.
  6. Reuse in multiple tests — Create the browser once in beforeAll, create the page object once, then use it in each test (and navigate or reset in beforeEach if needed).

Summary

StepAction
Set upCreate a pages/ folder and add page object classes (e.g. HomePage.js). Use the tests/ folder from npm init cstesting@latest for test files.
Page objectCreate a class that takes browser in the constructor and uses this.browser for actions. Export the class.
TestIn beforeAll, create the browser and the page object (new HomePage(browser)). In tests, call page object methods and use expect().
Runnpx cstesting tests/ or npx cstesting tests/home.test.js.

The Page Object Model in CSTesting is plain JavaScript classes and the standard browser API — no extra framework. You can use the same pattern in TypeScript by giving the browser parameter a type (e.g. BrowserApi from cstesting).