
Security News
Packagist Urges Immediate Composer Update After GitHub Actions Token Leak
Packagist urges PHP projects to update Composer after a GitHub token format change exposed some GitHub Actions tokens in CI logs.
POMWright is a complementary test framework for Playwright written in TypeScript.
POMWright is a TypeScript companion framework for Playwright focused on Page Object Model workflows.
It provides automatic locator chaining via a LocatorRegistry, a Session Storage API, a log fixture for report attachments, and a test.step decorator — all of which can be used independently either through a functional approachs or as part of your custom POMs or quickly create new standardized POMS by extending a class with the provided the abstract PageObject class. PageObject also adds navigation helpers and URL typing which supports multiple base URLs and dynamic RegExp paths.
POMWright now ships a v2-only runtime API centered on PageObject, LocatorRegistry, and standalone helpers.
Legacy v1 and migration documentation is retained under:
docs/v1docs/v1-to-v2-migrationPlaywright’s official best practices emphasize locator chaining for clarity and resilience. In practice, manually writing and maintaining those chains becomes tedious and fragile as pages grow. POMWright’s solution is the LocatorRegistry, which automatically builds locator chains from single locator definitions tied to unique paths. You register one locator per path (with Playwright‑like syntax), and POMWright composes the full chain for you.
This gives you the same Playwright primitives you already use — but with dramatically simpler maintenance, better structure, and safer refactors across small and large projects.
POMWright’s default behavior is to resolve locators by chaining all segments in a path. That brings two major benefits:
Robustness through scope‑aware uniqueness: Chaining narrows selectors through the intended DOM structure, producing more robust locators that are less brittle to UI changes and therefore less prone to flake. A “Change” button tied to a password field under a “Credentials” region resolves differently than a “Change” button tied to an email field under a “Contact info” region. If an identical button is added elsewhere, existing tests keep working because their paths remain scoped. If the button is mistakenly added under the wrong section, tests will fail and reveal the mistake.
Implicit DOM structure validation: Because chains traverse the DOM hierarchy, locator resolution validates that the UI is still arranged as expected — as strictly or loosely as you choose. You don’t need to map the entire DOM; just chain the “structural anchors” that matter. Often, user‑visible elements are good enough.
getNestedLocator) at the call site.getLocatorSchema(...).update/replace/remove/filter/nth/describe).PageObject provides URL + navigation + registry + session storage, leaving everything else to your own composition.PageObjectimport { test, type Locator, type Page } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly main: Locator;
readonly loginForm: Locator;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.main = page.locator("main");
this.loginForm = this.main.getByRole("form", { name: "Login" });
this.usernameInput = this.loginForm.getByLabel("Username");
this.passwordInput = this.loginForm.getByLabel("Password");
this.submitButton = this.main.getByRole("button", { name: "Login" });
}
async goto() {
await test.step("LoginPage.goto", async () => {
await this.page.goto("/login");
});
}
async login(username: string, password: string) {
await test.step("LoginPage.login", async () => {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
});
}
}
import { test as base } from "@playwright/test";
import { LoginPage } from "./login.page";
type Fixtures = { loginPage: LoginPage };
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
import { test } from "./fixtures";
test("login flow", async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login("alice", "secret");
});
PageObject (v2)import { type Page } from "@playwright/test";
import { PageObject, step } from "pomwright";
type Paths =
| "main"
| "main.form@login"
| "main.form@login.input@username"
| "main.form@login.input@password"
| "main.button@login";
export class LoginPage extends PageObject<Paths> {
constructor(page: Page) {
super(page, "https://example.com", "/login");
}
protected defineLocators(): void {
this.add("main").locator("main");
this.add("main.form@login").getByRole("form", { name: "Login" });
this.add("main.form@login.input@username").getByLabel("Username");
this.add("main.form@login.input@password").getByLabel("Password");
this.add("main.button@login").getByRole("button", { name: "Login" });
}
protected pageActionsToPerformAfterNavigation() {
return [
async () => {
await this.getNestedLocator("main.form@login").waitFor({ state: "visible" });
},
];
}
@step()
async login(username: string, password: string) {
await this.getNestedLocator("main.form@login.input@username").fill(username);
await this.getNestedLocator("main.form@login.input@password").fill(password);
await this.getNestedLocator("main.button@login").click();
}
}
Tip: If a Page Object grows large with many Paths and this.add(...) calls, move the locator definitions into a companion file (e.g., login.page.ts + login.locators.ts) to keep the class focused on behavior while keeping the registry definitions centralized and reusable. Simmilarily you can move all Paths and add calls for locator definitions common to all POMs for a given domain into a common.locators.ts file to share across your POMs.
// login.locators.ts
import type { LocatorRegistry } from "pomwright";
import { Paths as Common, defineLocators as addCommon } from "../common.locators"; // errors, dialogs, navbar, main, etc.
export type Paths =
| Common
| "main.form@login"
| "main.form@login.input@username"
| "main.form@login.input@password"
| "main.button@login";
export function defineLocators(registry: LocatorRegistry<Paths>) {
addCommon(registry);
registry.add("main.form@login").getByRole("form", { name: "Login" });
registry.add("main.form@login.input@username").getByLabel("Username");
registry.add("main.form@login.input@password").getByLabel("Password");
registry.add("main.button@login").getByRole("button", { name: "Login" });
}
// login.page.ts
import { type Page } from "@playwright/test";
import { PageObject, step } from "pomwright";
import { type Paths, defineLocators } from "./login.locators.ts";
export class LoginPage extends PageObject<Paths> {
constructor(page: Page) {
super(page, "https://example.com", "/login");
}
protected defineLocators(): void {
defineLocators(this.locatorRegistry);
}
protected pageActionsToPerformAfterNavigation() {
return [
async () => {
await this.getNestedLocator("common.nav.logo").waitFor({ state: "visible" });
await this.getNestedLocator("main.form@login").waitFor({ state: "visible" });
},
];
}
@step()
async login(username: string, password: string) {
await this.getNestedLocator("main.form@login.input@username").fill(username);
await this.getNestedLocator("main.form@login.input@password").fill(password);
await this.getNestedLocator("main.button@login").click();
}
}
import { test as base } from "@playwright/test";
import { PlaywrightReportLogger, type LogEntry, type LogLevel } from "pomwright";
import { LoginPage } from "./login.page";
type Fixtures = {
loginPage: LoginPage,
log: PlaywrightReportLogger
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
log: async ({}, use, testInfo) => { // or just import { test as base } from "pomwright";
const sharedLogEntry: LogEntry[] = [];
const sharedLogLevel: { current: LogLevel; initial: LogLevel } =
testInfo.retry === 0
? { current: "warn", initial: "warn" }
: { current: "debug", initial: "debug" };
const log = new PlaywrightReportLogger(sharedLogLevel, sharedLogEntry, "TestCase");
await use(log);
log.attachLogsToTest(testInfo);
},
});
import { test } from "./fixtures";
test("login flow", async ({ loginPage, log }) => {
await loginPage.navigation.gotoThisPage();
await loginPage.login("alice", "secret");
log.info("Hellow World!");
});
"main.form@login.input@username"), which makes intent clearer than referencing nested locator properties. Semantic paths make it obvious what a locator represents, while the registry tells you how it’s built.pnpm add -D pomwright
# or
npm install --save-dev pomwright
If you run into issues or have questions, please use the GitHub issues page.
Pull requests are welcome!
POMWright is open-source software licensed under Apache-2.0.
FAQs
POMWright is a complementary test framework for Playwright written in TypeScript.
The npm package pomwright receives a total of 45 weekly downloads. As such, pomwright popularity was classified as not popular.
We found that pomwright demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Packagist urges PHP projects to update Composer after a GitHub token format change exposed some GitHub Actions tokens in CI logs.

Research
GemStuffer abuses RubyGems as an exfiltration channel, packaging scraped UK council portal data into junk gems published from new accounts.

Company News
Socket was named to the Rising in Cyber 2026 list, recognizing 30 private cybersecurity startups selected by CISOs and security executives.