Skip to main content
@qawolf/pom provides Page Object Model (POM) infrastructure for Playwright: base classes for page objects, a registry that constructs page objects by name and collects their page hooks, 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 two ways to construct another page object: the static createFromPage(page) factory on a class you imported, and this.create(name) for a registered name. Keep selectors in a private locators getter.
dashboard-page.js may import login-page.js back, so two page objects that navigate to each other can both expose the trip. What Node cannot load is a page object that extends a class in a file importing it back — see Troubleshooting.

The page registry

The registry does two things: it constructs a page object from a name, and it is the list installPageHooks() walks to collect popup handlers and route interceptors. A direct import replaces the first job but not the second, so a page object that declares popupHandlers() or routeInterceptors() needs a registry entry however its instances get built. Constructing by name also keeps a page object’s module out of the calling file’s import graph until the first construction, which is worth having when a workspace holds many page objects.

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:
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:

Constructing

createPage builds a registered page object for a given Page. Import the registration module first so the registry is populated:
Inside a page object, use the protected this.create(...) instead — it shares the current Page and goes through the same registry. Import the sibling’s type for the return annotation:
create / createPage are async because lazily registered modules load on first use. Importing SettingsPage and calling SettingsPage.createFromPage(this.page) is the synchronous alternative, and needs no registry entry and no separate return-type annotation.

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.
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:
A page object can own popups to auto-dismiss or routes to intercept by overriding popupHandlers() / routeInterceptors() on its class:
installPageHooks() collects these across every registered page object, not just the entry point — so register a page object that owns popups or routes even when your code only ever constructs it from a direct import. Its hooks never install otherwise. 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:

Exports

Last modified on August 13, 2026