@qawolf/pom provides Page Object Model (POM) infrastructure for
Playwright: base classes for page objects, a central
registry that lets page objects construct one another without circular imports,
and automatic installation of popup and route-interception hooks.
Requirements
- Node.js
>=22.22
- ES modules — the package is ESM-only (
"type": "module"). Import it from
ESM code and use explicit .js specifiers in relative imports.
- Peer dependencies
@qawolf/flows — used by EntryPointPageObject to launch the browser.
playwright — for the Page / Locator types your page objects use.
Install
Defining a page object
Extend BasePageObject. It stores the Playwright Page (available as
this.page) and provides this.create(...) for constructing other page
objects. Keep selectors in a private locators getter.
import { BasePageObject } from "@qawolf/pom";
export class LoginPage extends BasePageObject {
private get locators() {
return {
email: this.page.getByLabel("Email"),
password: this.page.getByLabel("Password"),
submit: this.page.getByRole("button", { name: "Sign in" }),
} as const;
}
async signIn(email: string, password: string) {
await this.locators.email.fill(email);
await this.locators.password.fill(password);
await this.locators.submit.click();
return this.create("DashboardPage");
}
}
The page registry
Registering
Create one module that registers every page object, and import it for its side
effects before you construct any page object. Register lazily with a module
loader so a page object’s module is only loaded when it is first used:
// register-pages.ts
import { registerPage } from "@qawolf/pom";
registerPage("LoginPage", () => import("./pages/login-page.js"));
registerPage("DashboardPage", () => import("./pages/dashboard-page.js"));
The loader must resolve to a module that exports the class under the same
name it was registered with. Eager registration also works when you already
hold the class value:
import { LoginPage } from "./pages/login-page.js";
registerPage("LoginPage", LoginPage);
Constructing
createPage builds a registered page object for a given Page. Import the
registration module first so the registry is populated:
import "./register-pages.js";
import { createPage } from "@qawolf/pom";
const loginPage = await createPage("LoginPage", page);
await loginPage.signIn("user@example.com", "hunter2");
Inside a page object, use the protected this.create(...) instead — it shares
the current Page and routes through the same registry, which is what lets
page objects reference each other without importing each other’s classes as
values (avoiding circular imports). Use import type only for annotations:
import { BasePageObject } from "@qawolf/pom";
import type { SettingsPage } from "./settings-page.js";
export class DashboardPage extends BasePageObject {
async openSettings(): Promise<SettingsPage> {
await this.page.getByRole("link", { name: "Settings" }).click();
return this.create("SettingsPage");
}
}
create / createPage are async because lazily registered modules load on
first use.
Typing this.create(...)
By default this.create("LoginPage") returns any. Augment the
RegisteredPages interface — declared for exactly this purpose — to make the
name-to-type mapping known, and create becomes fully typed. This is
incremental: names you don’t list keep the any fallback.
import type { LoginPage } from "./pages/login-page.js";
import type { DashboardPage } from "./pages/dashboard-page.js";
declare module "@qawolf/pom" {
interface RegisteredPages {
LoginPage: LoginPage;
DashboardPage: DashboardPage;
}
}
Now await this.create("LoginPage") is typed as LoginPage.
Entry points and page hooks
An entry point is the page object that owns browser startup. Extend
EntryPointPageObject and expose a create() that launches the browser
(initializeBrowser), builds the instance (createFromPage), and installs
page hooks:
import { EntryPointPageObject } from "@qawolf/pom";
export class AppEntryPoint extends EntryPointPageObject {
static async create(): Promise<AppEntryPoint> {
const page = await AppEntryPoint.initializeBrowser();
const entryPoint = AppEntryPoint.createFromPage(page);
await entryPoint.installPageHooks();
return entryPoint;
}
async openLogin() {
await this.goto("/login");
return this.create("LoginPage");
}
}
A page object can own popups to auto-dismiss or routes to intercept by
overriding popupHandlers() / routeInterceptors() on its class:
import { BasePageObject, type PopupHandlerDef } from "@qawolf/pom";
export class CookieBannerPage extends BasePageObject {
override popupHandlers(): PopupHandlerDef[] {
return [
{
name: "cookie-banner",
trigger: this.page.getByRole("button", { name: "Accept cookies" }),
dismiss: async () => {
await this.page
.getByRole("button", { name: "Accept cookies" })
.click();
},
},
];
}
}
installPageHooks() collects these across every registered page object,
not just the entry point. Overrides are detected by an own-property check on the
registered class’s prototype, so declare popupHandlers() /
routeInterceptors() directly on the class you register — an override inherited
from a base class is not picked up.
When registering lazily, pass { providesPageHooks: false } for page objects
you know declare no hooks, so hook installation can skip loading their modules:
registerPage("StaticPage", () => import("./pages/static-page.js"), {
providesPageHooks: false,
});
Exports
| Export | Description |
|---|
BasePageObject | Base class for page objects. Holds this.page; provides this.create(name) and the static createFromPage(page) factory. |
SubPageObject | Base class for a page object scoped to a parent region/component. |
EntryPointPageObject | Base class for the entry point; owns browser launch (initializeBrowser), goto, and installPageHooks. |
registerPage(name, classOrLoader, options?) | Register a page object by name, eagerly (class) or lazily (() => import(...)). |
createPage(name, page) | Construct a registered page object for a Page. Returns a promise. |
PopupHandler | Manages add/remove of popup handlers on a page. |
NetworkMonitor | Observes network activity and surfaces NetworkErrors. |
RegisteredPages | Empty interface you augment to type create(name). |
PopupHandlerDef, RouteInterceptorDef, PageSetupOptions | Hook and setup types. |
callPlatformAPI | Client for the QA Wolf platform API. |
assertPricesClose, moneyToNumber, numberToMoney | Money/price test-data helpers. |
reportCleanupFailed, reportCleanupFailure | Report failures from test cleanup steps. |
Last modified on July 7, 2026