npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@qawolf/pom

v0.6.1

Published

`@qawolf/pom` is a TypeScript toolkit for building Page Object Models (POMs) on top of Playwright: base classes for page objects, construction of sibling page objects by class or by name, and automatic popup and route-hook installation.

Readme

@qawolf/pom

@qawolf/pom is a TypeScript toolkit for building Page Object Models (POMs) on top of Playwright: base classes for page objects, construction of sibling page objects by class or by name, and automatic popup and route-hook installation.

It is built for and maintained as part of the QA Wolf platform, and it is the foundation the POMs in QA Wolf workspaces are generated against. The core page-object building blocks are written against plain Playwright APIs, while the package as a whole expects the QA Wolf runtime to be present (see Platform integration below).

To learn more about how to use @qawolf/pom, check out the API Reference and Documentation.

Requirements

  • Node.js >=22.22.0 <25
  • Playwright 1.58.2 (peer dependency)
  • @qawolf/flows ^0.1.4 (peer dependency, supplied by the QA Wolf runner)

Both are optional peers, so npm will not install them for you. That is deliberate for @qawolf/flows: a second copy of it on the module path breaks a run, because Playwright refuses to load twice in one process. The QA Wolf runner supplies flows at runtime, so a QA Wolf workspace needs nothing here. Outside one, add @qawolf/flows to your own dependencies, or importing @qawolf/pom throws ERR_MODULE_NOT_FOUND.

Install

npm install @qawolf/pom

Usage

Define a page object by extending BasePageObject, keeping selectors in a private locators getter. To hand off to the next page object, import it and build it from the current Playwright page.

import { BasePageObject } from "@qawolf/pom";

import { DashboardPage } from "./dashboard-page.js";

export class LoginPage extends BasePageObject {
  private get locators() {
    return {
      email: this.page.getByLabel("Email"),
      submit: this.page.getByRole("button", { name: "Sign in" }),
    } as const;
  }

  async signIn(email: string) {
    await this.locators.email.fill(email);
    await this.locators.submit.click();
    return DashboardPage.createFromPage(this.page);
  }
}

dashboard-page.js may import login-page.js back, so two pages that navigate to each other can both expose the trip. The one shape to avoid is a page object that extends a class in a file that imports it back — whichever of the two loads second throws Cannot access 'X' before initialization.

Constructing a sibling

Inside a page object, pass the sibling's class to create:

import { BasePageObject } from "@qawolf/pom";

import { DashboardPage } from "../primary/dashboard-page.js";

export class LoginPage extends BasePageObject {
  async signIn() {
    return this.create(DashboardPage);
  }
}

The return type is inferred from the class, so there is no name to repeat and no annotation to drift from it, and nothing is resolved at runtime. Passing a type-only import is a compile error, which is the point: the class must be a value.

Constructing by name

A page object can also be built from its class name, resolved through the calling file's own imports:

import { BasePageObject } from "@qawolf/pom";

import { DashboardPage } from "../primary/dashboard-page.js";

export class LoginPage extends BasePageObject {
  async signIn() {
    return this.create<DashboardPage>("DashboardPage");
  }
}

The import must be a value import: the specifier is read from the executing file's source, and compilation erases a type-only import, so on the QA Wolf runner an import type binding leaves nothing to resolve through (the require-value-import-for-created-page lint rule catches this at edit time). A name that no import binds does not resolve. The module must export the class under that name. Annotate the call for a return type more specific than BasePageObject — and note that nothing checks the annotation and the name agree, which is the other reason to prefer passing the class.

create is deliberately protected and has no flow-facing counterpart: flow code never holds a Playwright Page, so it cannot construct page objects directly. A flow gets its first POM from an entry point's static create(), and every subsequent one from methods on POMs it already has.

Page hooks

An entry point declares the popups to dismiss and the routes to intercept on every page of every browser it launches. The declarations are static because they are installed on the browser context before its first page exists, so each def is built against whichever page it ends up applying to:

import { EntryPointPageObject } from "@qawolf/pom";
import type { PopupHandlerDef, RouteInterceptorDef } from "@qawolf/pom";

export class LoginPage extends EntryPointPageObject {
  static async create(options?: PageSetupOptions): Promise<LoginPage> {
    const page = await this.initializeBrowser(options);
    return new this(page);
  }

  protected static override popupHandlers(): PopupHandlerDef[] {
    return [
      {
        cssSelector: "#cookie-consent",
        dismiss: (page) => page.locator("#cookie-consent .accept").click(),
        name: "cookie-banner",
        trigger: (page) => page.locator("#cookie-consent"),
      },
    ];
  }

  protected static override routeInterceptors(): RouteInterceptorDef[] {
    return [
      {
        handler: (route) => route.abort(),
        name: "block-analytics",
        pattern: "**/analytics/**",
      },
    ];
  }
}

initializeBrowser() launches the browser, installs the hooks on its context, and returns the first page; create() adds whatever else the first page needs — a goto, a sign-in. Because the hooks live on the context, 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. Other page objects declare nothing.

The declared hooks are the default. A flow adjusts them through the options it passes to create():

// Test the cookie banner itself, and let analytics through.
await LoginPage.create({ allowPopups: ["cookie-banner"], allowRoutes: "all" });

// Block one more endpoint, in this flow only.
await LoginPage.create({
  routeInterceptors: [
    {
      handler: (route) => route.abort(),
      name: "block-chat",
      pattern: "**/chat/**",
    },
  ],
});

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. A flow-owned PopupHandler (handler) takes over every popup on the entry point's page; it is a one-per-page object, so it covers the first page only.

Network errors

Every browser an entry point launches records its 4xx/5xx responses — from the first page, a second tab, a popup window alike — on a NetworkMonitor bound to the browser context. Asserting is the flow's choice:

const login = await LoginPage.create();
// ...
login.networkMonitor.assertClean({ exclude: [/analytics/] });

Pass monitor: false to create() to turn recording off for a flow, or a NetworkMonitor of the flow's own to have that one installed instead.

Migrating from the page registry

registerPage, createPage and the RegisteredPages augmentation are gone. To move a workspace off register-pages.ts:

  • Delete register-pages.ts, including its declare module "@qawolf/pom" block, and every import of it.
  • Where a page object calls this.create("Name"), import the class as a value and pass it: this.create(Name). The name form keeps working given a value import, but a type-only one no longer resolves anywhere.
  • Where a page object declares popupHandlers() / routeInterceptors(), move the declaration onto the entry point as a static method. trigger and dismiss now take the page they apply to, since the hooks cover every page of the browser context, and installPageHooks() is gone — initializeBrowser() installs them.
  • Flow code that called createPage("Name", page) gets its page objects from methods on the entry point instead.

Platform integration

@qawolf/pom is designed to run on the QA Wolf platform, and a few of its features talk to that platform directly:

  • EntryPointPageObject launches the browser through @qawolf/flows in a QA Wolf run context.
  • Cleanup reporting (reportCleanupFailure) posts cleanup failures to the QA Wolf automation API when a run is active.
  • callPlatformAPI calls the QA Wolf platform tRPC API and requires a QAW_TOKEN.

These features read their configuration from the environment provided by a QA Wolf run, including:

| Variable | Purpose | | -------------------------------------------------------------------------- | --------------------------------------------------------------- | | QAW_TOKEN | Auth token for QA Wolf platform API calls (callPlatformAPI). | | DEFAULT_URL | Default URL used by EntryPointPageObject.goto(). | | AUTH_USERNAME, AUTH_PASSWORD | Optional HTTP basic-auth credentials applied at browser launch. | | QAWOLF_RUN_ID, QAWOLF_SUITE_ID, QAWOLF_TEAM_ID, QAWOLF_WORKFLOW_ID | Run metadata attached to cleanup failure reports. |

The core building blocks (BasePageObject, SubPageObject, and popup and route hooks) do not call the QA Wolf platform themselves. They still need @qawolf/flows to resolve, because the package entry point re-exports EntryPointPageObject and NetworkMonitor, which import it.

License

MIT © QA Wolf Inc.