Skip to main content
@qawolf/pom provides Page Object Model (POM) infrastructure for Playwright: base classes for page objects, construction of sibling page objects by class or by name, and automatic installation of popup and route-interception hooks. QA Wolf can enforce conventions on the page objects you and its AI write — see Lint rules.

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(...). 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.

Constructing a sibling

Inside a page object, pass the sibling’s class to the protected this.create(...). It shares the current Page:
The return type is inferred from the class, so there is no name to repeat and no annotation to drift from it. Passing a type-only import is a compile error: the class must be a value.

Constructing by name

this.create("Name") also accepts a class name, resolved through the calling file’s own imports:
The import must be a value import: the specifier is read from the executing file’s source, and compilation erases a type-only import. A name that no import binds does not resolve. The module must export the class under that name. Without the generic, the return type is BasePageObject, and nothing checks that the generic and the name agree — another reason to prefer passing the class. create is async so both forms are interchangeable at a call site. Flow code never holds a Page, so it gets its first page object from an entry point’s static create() and every later one from methods on page objects it already has.

Entry points and page hooks

An entry point is the page object that owns browser startup. Extend EntryPointPageObject and expose a static create() that launches the browser with initializeBrowser and builds the instance:
The entry point declares the popups to dismiss and the routes to intercept by overriding the static popupHandlers() / routeInterceptors(). Other page objects declare none. The declarations are static because they are installed on the browser context before its first page exists, so trigger and dismiss take the page they apply to:
initializeBrowser() installs these on the browser context, so a second tab or a popup window the app opens carries them too. A popup with a cssSelector is hidden by a <style> tag injected before any navigation; one without falls back to addLocatorHandler on each page. super.popupHandlers() extends a parent entry point’s list. A flow adjusts the declared hooks through the options it passes to create():
allowPopups / allowRoutes skip declared hooks by name, or all of them with "all"; popupHandlers / routeInterceptors add the flow’s own, a same-named one replacing the declared one.

Exports

Last modified on October 6, 2026