POMWright is a TypeScript-based framework that implements the Page Object Model Design Pattern, designed specifically to augment Playwright's testing capabilities.
POMWright provides a way of abstracting the implementation details of a web page and encapsulating them into a reusable page object. This approach makes the tests easier to read, write, and maintain, and helps reduce duplicated code by breaking down the code into smaller, reusable components, making the code more maintainable and organized.
Simply extend a class with BasePage
to create a Page Object Class (POC).
Define different base URLs by extending an abstract class with BasePage
per domain and have your POCs for each domain extend the abstract classes.
Seamlessly integrate custom Playwright Fixtures with your POMWright POCs.
Define comprehensive locators for each POC and share common locators between them.
Efficiently manage and chain locators through LocatorSchemaPath
s.
Modify single or multiple locators within a chained locator dynamically during tests using the new .update()
and .addFilter()
methods.
Ensure that original LocatorSchemas
remain immutable and reusable across tests.
Gain insights with detailed logs for nested locators, integrated with Playwright's HTML report. Or use the Log fixture throughout your own POCs and tests to easily attach them to the HTML report based on log levels.
Enhance your tests with advanced sessionStorage
handling capabilities.
- New
filter
Property: Apply filters across various locator types beyond just thelocator
method. - New
.addFilter()
Method: Dynamically add filters to any part of the locator chain.
Ensure you have Node.js installed, then run:
npm install pomwright --save-dev
or
pnpm i -D pomwright
Explore POMWright in action by diving into the example project located in the "example" folder. Follow these steps to get started:
Navigate to the "example" folder and install the necessary dependencies:
pnpm install
Install or update Playwright browsers:
pnpm playwright install --with-deps
Execute tests across Chromium, Firefox, and WebKit:
pnpm playwright test
Control parallel test execution. By default, up to 4 tests run in parallel. Modify this setting as needed:
pnpm playwright test --workers 2 # Set the number of parallel workers
After the tests complete, a Playwright HTML report is available in ./example/playwright-report
. Open the index.html
file in your browser to view the results.
Dive into using POMWright with these examples:
import { Page, TestInfo } from "@playwright/test";
import { BasePage, PlaywrightReportLogger, GetByMethod } from "pomwright";
type LocatorSchemaPath =
| "content"
| "content.heading"
| "content.region.details"
| "content.region.details.button.edit";
export default class Profile extends BasePage<LocatorSchemaPath> {
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
super(page, testInfo, "https://someDomain.com", "/profile", Profile.name, pwrl);
}
protected initLocatorSchemas() {
this.locators.addSchema("content", {
locator: ".main-content",
locatorMethod: GetByMethod.locator
});
this.locators.addSchema("content.heading", {
role: "heading",
roleOptions: {
name: "Your Profile"
},
locatorMethod: GetByMethod.role
});
this.locators.addSchema("content.region.details", {
role: "region",
roleOptions: {
name: "Profile Details"
},
locatorMethod: GetByMethod.role
});
this.locators.addSchema("content.region.details.button.edit", {
role: "button",
roleOptions: {
name: "Edit"
},
locatorMethod: GetByMethod.role
});
}
// add your helper methods here...
}
import { test as base } from "pomwright";
import Profile from "...";
type fixtures = {
profile: Profile;
}
export const test = base.extend<fixtures>({
profile: async ({ page, log }, use, testInfo) => {
const profile = new Profile(page, testInfo, log);
await use(profile);
}
});
import { test } from ".../fixtures";
test("click edit button with a single locator", async ({ profile }) => {
// perform setup/navigation...
await profile.page.waitForURL(profile.fullUrl);
/**
* returns the locator resolving to the full locatorSchemaPath
*/
const editBtn = await profile.getLocator("content.region.details.button.edit");
await editBtn.click();
});
import { test } from ".../fixtures";
test("click edit button with a nested locator", async ({ profile }) => {
// perform setup/navigation...
await profile.page.waitForURL(profile.fullUrl);
/**
* returns a nested/chained locator consisting of the 3 locators:
* content,
* content.region.details
* content.region.details.button.edit
*/
const editBtn = await profile.getNestedLocator("content.region.details.button.edit");
await editBtn.click();
});
import { test } from ".../fixtures";
test("specify index for nested locator(s)", async ({ profile }) => {
// perform setup/navigation...
await profile.page.waitForURL(profile.fullUrl);
/**
* returns a nested/chained locator consisting of the 3 locators:
* content
* content.region.details with .first()
* content.region.details.button.edit with .nth(1)
*/
const editBtn = await profile.getNestedLocator("content.region.details.button.edit", {
"content.region.details": 0,
"content.region.details.button.edit": 1
});
await editBtn.click();
});
import { test } from ".../fixtures";
test("update a locator before use", async ({ profile }) => {
// perform setup/navigation...
await profile.page.waitForURL(profile.fullUrl);
/**
* returns a nested/chained locator consisting of the 3 locators:
* content,
* content.region.details
* content.region.details.button.edit (updated)
*/
const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
.update("content.region.details.button.edit", {
roleOptions: { name: "Edit details" }
})
.getNestedLocator();
await editBtn.click();
});
import { test } from ".../fixtures";
test("update a nested locator before use", async ({ profile }) => {
// perform setup/navigation...
await profile.page.waitForURL(profile.fullUrl);
/**
* returns a nested/chained locator consisting of the 3 locators:
* content,
* content.region.details (updated)
* content.region.details.button.edit
*/
const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
.update("content.region.details", {
locator: ".profile-details",
locatorMethod: GetByMethod.locator
})
.getNestedLocator();
await editBtn.click();
});
import { test } from ".../fixtures";
test("make multiple versions of a locator", async ({ profile }) => {
// perform setup/navigation...
await profile.page.waitForURL(profile.fullUrl);
const editBtnSchema = profile.getLocatorSchema("content.region.details.button.edit");
const editBtn = await editBtnSchema.getLocator();
await editBtn.click();
editBtnSchema.update("content.region.details.button.edit", { roleOptions: { name: "Edit details" } });
const editBtnUpdated = await editBtnSchema.getNestedLocator();
await editBtnUpdated.click();
/**
* Calling profile.getLocatorSchema("content.region.details.button.edit") again
* will return a new deepCopy of the original LocatorSchema
*/
});
- Old
.update(updates: Partial<LocatorSchemaWithoutPath>): LocatorSchemaWithMethods
- Old
.updates(indexedUpdates: { [index: number]: Partial<LocatorSchemaWithoutPath> | null }): LocatorSchemaWithMethods
Reason:
The .updates
method relied on index-based updates, which are prone to errors and require manual maintenance, especially when LocatorSchemaPath
strings are renamed or restructured. Additionally, the old .update
method could only update the last LocatorSchema
in the chain, making it less flexible.
Replacement:
Use the new .update(subPath, modifiedSchema)
method, which leverages valid subPath
s of LocatorSchemaPath
strings for more intuitive and maintainable updates.
Removal Schedule:
These methods are deprecated and will be removed in version 2.0.0.
- Old
getNestedLocator(indices?: { [key: number]: number | null }): Promise<Locator>
Reason:
Index-based indexing is less readable and requires manual updates when LocatorSchemaPath
strings change.
Replacement:
Use the updated getNestedLocator(subPathIndices?: { [K in SubPaths<LocatorSchemaPathType, LocatorSubstring>]: number | null }): Promise<Locator>
method, which utilizes LocatorSchemaPath
strings for indexing.
Removal Schedule:
This method is deprecated and will be removed in version 2.0.0.
Old Usage:
const allCheckboxes = await poc
.getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
.updates({ 3: { locatorOptions: { hasText: /Producer/i } } })
.getNestedLocator();
New Usage:
const allCheckboxes = await poc
.getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
.update("main.products.searchControls.filterType", { locatorOptions: { hasText: /Producer/i } })
.getNestedLocator();
Old Usage (Deprecated):
const saveBtn = await profile.getNestedLocator("content.region.details.button.save", { 4: 2 });
const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
.getNestedLocator({ 2: index });
New Usage:
const saveBtn = await profile.getNestedLocator("content.region.details.button.save", {
"content.region.details.button.save": 2
});
const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
.getNestedLocator({ "content.region.details": index });
Transition from index-based to LocatorSchemaPath
-based indexing to improve code readability and maintainability.
Old Example:
const allCheckboxes = await poc
.getLocatorSchema("main.form.item.checkbox")
.updates({ 3: { locatorOptions: { hasText: /Producer/i } } })
.getNestedLocator();
New Example:
const allCheckboxes = await poc
.getLocatorSchema("main.form.item.checkbox")
.update("main.form.item", { locatorOptions: { hasText: /Producer/i } })
.getNestedLocator();
// Defining LocatorSchemas
this.locators.addSchema("body.main.section@userInfo", {
role: "region",
roleOptions: { name: "Contact Info" },
filter: { hasText: /e-mail/i },
locatorMethod: GetByMethod.role
});
// Dynamically adding additional filters using `.addFilter()`
const specificSection = await poc
.getLocatorSchema("body.main.section@userInfo")
.addFilter("body.main.section@userInfo", { hasText: "Additional Services" })
.getNestedLocator();
const editBtn = await profile
.getLocatorSchema("content.region.details.button.edit")
.update("content.region.details.button.edit", {
roleOptions: { name: "new accessibility name" }
})
.getNestedLocator();
import { GetByMethod, LocatorSchemaWithoutPath } from "pomwright";
import { missingInputError } from "@common/page-components/errors.locatorSchema.ts";
export type LocatorSchemaPath =
| "body"
| "body.main"
| "body.main.section"
| "body.main.section@products"
| "body.main.section@userInfo"
| "[email protected]@email"
| "[email protected]"
| "body.main.section@deliveryInfo";
export function initLocatorSchemas(locators: GetLocatorBase<LocatorSchemaPath>) {
locators.addSchema("body", {
locator: "body",
locatorMethod: GetByMethod.locator,
});
locators.addSchema("body.main", {
locator: "main",
locatorMethod: GetByMethod.locator,
});
const region: LocatorSchemaWithoutPath = { role: "region", locatorMethod: GetByMethod.role };
locators.addSchema("body.main.section", {
...region,
});
locators.addSchema("body.main.section@products", {
...region,
roleOptions: { name: "Products" },
});
locators.addSchema("body.main.section@userInfo", {
...region,
roleOptions: { name: "Contact Info" },
filter: { hasText: /e-mail/i },
});
locators.addSchema("[email protected]@email", {
role: "textbox",
roleOptions: { name: "Input your e-mail" },
locatorMethod: GetByMethod.role,
});
locators.addSchema("[email protected]", {
...missingInputError,
});
locators.addSchema("body.main.section@deliveryInfo", {
...region,
roleOptions: { name: "Delivery Info" },
});
}
If you encounter any issues or have questions, please check our issues page or reach out to us directly.
Pull Requests are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.
POMWright is open-source software licensed under the Apache-2.0 license.