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:
- Scaffolding a
pages/andtests/structure with sample code vianpx cstesting init - Letting you pass the browser into page object classes so they call
browser.goto(),browser.click(),browser.locator(), etc.
This guide describes how to use POM with CSTesting.
You will learn
- What the Page Object Model is and why to use it
- How to scaffold the structure with
cstesting init - 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 ofbrowser.type('[name="userName"]', 'admin'). - Reuse — The same page object can be used in many tests and suites.
- Clear structure —
pages/holds page classes;tests/holds test files.
Scaffold the structure
From your project root, run:
npx cstesting init
# or
npx cst init
This creates:
pages/— folder for page object classestests/— folder for test filespages/HomePage.js— sample page object for example.comtests/home.test.js— sample test that usesHomePage
Existing files are not overwritten. You can add more page objects and tests as needed.
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
browserand expose methods (e.g.goto(),clickSubmit(),getHeadingText()). They usethis.browserto callgoto,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 useexpect()for assertions.
Example: HomePage (scaffolded)
The scaffolded pages/HomePage.js looks like this:
/**
* 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:
| Method | Use 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
- One class per page (or major section) — e.g.
HomePage,LoginPage,DashboardPage. - Constructor takes
browser—constructor(browser) { this.browser = browser; }so the same browser is reused across page objects if needed. - Expose actions and getters, hide selectors — Tests call
loginPage.typeUsername('x'), notbrowser.type('[name="userName"]', 'x'). - Optional: expose URL — A
urlgetter orgoto()method keeps navigation in one place. - Use locators when it helps — e.g.
this.browser.locator('button.primary').click()inside the page object to keep selectors out of tests. - 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 inbeforeEachif needed).
Summary
| Step | Action |
|---|---|
| Scaffold | Run npx cstesting init to create pages/ and tests/ with sample HomePage and home.test.js. |
| Page object | Create a class that takes browser in the constructor and uses this.browser for actions. Export the class. |
| Test | In beforeAll, create the browser and the page object (new HomePage(browser)). In tests, call page object methods and use expect(). |
| Run | npx 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).