Skip to main content
@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

npm install @qawolf/pom

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

ExportDescription
BasePageObjectBase class for page objects. Holds this.page; provides this.create(name) and the static createFromPage(page) factory.
SubPageObjectBase class for a page object scoped to a parent region/component.
EntryPointPageObjectBase 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.
PopupHandlerManages add/remove of popup handlers on a page.
NetworkMonitorObserves network activity and surfaces NetworkErrors.
RegisteredPagesEmpty interface you augment to type create(name).
PopupHandlerDef, RouteInterceptorDef, PageSetupOptionsHook and setup types.
callPlatformAPIClient for the QA Wolf platform API.
assertPricesClose, moneyToNumber, numberToMoneyMoney/price test-data helpers.
reportCleanupFailed, reportCleanupFailureReport failures from test cleanup steps.
Last modified on July 7, 2026