@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