@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